diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 7aa8528dc3d9..0edba11836fc 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -98,7 +98,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: ci-group-4d + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 retry: diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 1da3f654b725..404bfb273b6f 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -96,7 +96,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: ci-group-4d + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 retry: diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh index 89121581c75d..ac80a66d33fa 100755 --- a/.buildkite/scripts/common/env.sh +++ b/.buildkite/scripts/common/env.sh @@ -56,8 +56,8 @@ else fi # These are for backwards-compatibility -export GIT_COMMIT="$BUILDKITE_COMMIT" -export GIT_BRANCH="$BUILDKITE_BRANCH" +export GIT_COMMIT="${BUILDKITE_COMMIT:-}" +export GIT_BRANCH="${BUILDKITE_BRANCH:-}" export FLEET_PACKAGE_REGISTRY_PORT=6104 export TEST_CORS_SERVER_PORT=6105 diff --git a/.buildkite/scripts/packer_cache.sh b/.buildkite/scripts/packer_cache.sh index 45d3dc439ff4..617ea79c827b 100755 --- a/.buildkite/scripts/packer_cache.sh +++ b/.buildkite/scripts/packer_cache.sh @@ -2,6 +2,7 @@ set -euo pipefail +source .buildkite/scripts/common/util.sh source .buildkite/scripts/common/env.sh source .buildkite/scripts/common/setup_node.sh diff --git a/.buildkite/scripts/steps/functional/oss_misc.sh b/.buildkite/scripts/steps/functional/oss_misc.sh index a57a457ca189..48be6669f321 100755 --- a/.buildkite/scripts/steps/functional/oss_misc.sh +++ b/.buildkite/scripts/steps/functional/oss_misc.sh @@ -2,9 +2,6 @@ set -euo pipefail -# Required, at least for kbn_sample_panel_action -export BUILD_TS_REFS_DISABLE=false - source .buildkite/scripts/steps/functional/common.sh # Required, at least for plugin_functional tests diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 90f88de7d110..244689025173 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -62,6 +62,7 @@ /packages/kbn-interpreter/ @elastic/kibana-app-services /src/plugins/bfetch/ @elastic/kibana-app-services /src/plugins/data/ @elastic/kibana-app-services +/src/plugins/data-views/ @elastic/kibana-app-services /src/plugins/embeddable/ @elastic/kibana-app-services /src/plugins/expressions/ @elastic/kibana-app-services /src/plugins/field_formats/ @elastic/kibana-app-services @@ -242,7 +243,6 @@ /packages/kbn-std/ @elastic/kibana-core /packages/kbn-config/ @elastic/kibana-core /packages/kbn-logging/ @elastic/kibana-core -/packages/kbn-legacy-logging/ @elastic/kibana-core /packages/kbn-crypto/ @elastic/kibana-core /packages/kbn-http-tools/ @elastic/kibana-core /src/plugins/saved_objects_management/ @elastic/kibana-core @@ -447,4 +447,5 @@ /docs/setup/configuring-reporting.asciidoc @elastic/kibana-reporting-services @elastic/kibana-app-services #CC# /x-pack/plugins/reporting/ @elastic/kibana-reporting-services - +# EUI design +/src/plugins/kibana_react/public/page_template/ @elastic/eui-design @elastic/kibana-app-services diff --git a/.i18nrc.json b/.i18nrc.json index 4107772e421c..45016edc38dc 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -9,6 +9,7 @@ "bfetch": "src/plugins/bfetch", "dashboard": "src/plugins/dashboard", "data": "src/plugins/data", + "dataViews": "src/plugins/data_views", "embeddableApi": "src/plugins/embeddable", "embeddableExamples": "examples/embeddable_examples", "fieldFormats": "src/plugins/field_formats", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14dfaa84cebb..8a19562eaff4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,5 @@ # Contributing to Kibana -If you are an employee at Elastic, please check out our Developer Guide [here](https://docs.elastic.dev/kibana-dev-docs/welcome). +If you are an employee at Elastic, please check out our Developer Guide [here](https://docs.elastic.dev/kibana-dev-docs/getting-started/welcome). If you are an external developer, we have a legacy developer guide [here](https://www.elastic.co/guide/en/kibana/master/development.html), or you can view the raw docs from our new, internal Developer Guide [here](./dev_docs/getting_started/dev_welcome.mdx). Eventually, our internal Developer Guide will be opened for public consumption. diff --git a/STYLEGUIDE.mdx b/STYLEGUIDE.mdx index 95f29c674da9..56117d0fd7e5 100644 --- a/STYLEGUIDE.mdx +++ b/STYLEGUIDE.mdx @@ -1,6 +1,6 @@ --- id: kibStyleGuide -slug: /kibana-dev-docs/styleguide +slug: /kibana-dev-docs/contributing/styleguide title: Style Guide summary: JavaScript/TypeScript styleguide. date: 2021-05-06 diff --git a/config/kibana.yml b/config/kibana.yml index dea9849f17b2..13a4b9bb98e8 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -84,24 +84,32 @@ # Time in milliseconds for Elasticsearch to wait for responses from shards. Set to 0 to disable. #elasticsearch.shardTimeout: 30000 -# Logs queries sent to Elasticsearch. Requires logging.verbose set to true. -#elasticsearch.logQueries: false - # Specifies the path where Kibana creates the process ID file. #pid.file: /run/kibana/kibana.pid +# Set the value of this setting to off to suppress all logging output, or to debug to log everything. +# logging.root.level: debug + # Enables you to specify a file where Kibana stores log output. -#logging.dest: stdout +# logging.appenders.default: +# type: file +# fileName: /var/logs/kibana.log + -# Set the value of this setting to true to suppress all logging output. -#logging.silent: false +# Logs queries sent to Elasticsearch. +# logging.loggers: +# - name: elasticsearch.queries +# level: debug -# Set the value of this setting to true to suppress all logging output other than error messages. -#logging.quiet: false +# Logs http responses. +# logging.loggers: +# - name: http.server.response +# level: debug -# Set the value of this setting to true to log all events, including system usage information -# and all requests. -#logging.verbose: false +# Logs system usage information. +# logging.loggers: +# - name: metrics.ops +# level: debug # Set the interval in milliseconds to sample system and process performance # metrics. Minimum is 100ms. Defaults to 5000. diff --git a/dev_docs/api_welcome.mdx b/dev_docs/api_welcome.mdx index be9281113b3d..00d5bfb9644a 100644 --- a/dev_docs/api_welcome.mdx +++ b/dev_docs/api_welcome.mdx @@ -1,6 +1,6 @@ --- id: kibDevDocsApiWelcome -slug: /kibana-dev-docs/api-welcome +slug: /kibana-dev-docs/api-meta/welcome title: Welcome summary: How to use our automatically generated API documentation date: 2021-02-25 diff --git a/dev_docs/best_practices.mdx b/dev_docs/contributing/best_practices.mdx similarity index 98% rename from dev_docs/best_practices.mdx rename to dev_docs/contributing/best_practices.mdx index 767e525c0afa..284baababfc6 100644 --- a/dev_docs/best_practices.mdx +++ b/dev_docs/contributing/best_practices.mdx @@ -1,6 +1,6 @@ --- id: kibBestPractices -slug: /kibana-dev-docs/best-practices +slug: /kibana-dev-docs/contributing/best-practices title: Best practices summary: Best practices to follow when building a Kibana plugin. date: 2021-03-17 @@ -111,7 +111,7 @@ export getSearchService: (searchSpec: { username: string; password: string }) => In the former, there will be a link to the `SearchSpec` interface with documentation for the `username` and `password` properties. In the latter the object will render inline, without comments: -![prefer interfaces documentation](./assets/dev_docs_nested_object.png) +![prefer interfaces documentation](../assets/dev_docs_nested_object.png) #### Export every type used in a public API @@ -135,7 +135,7 @@ export type foo: string | AnInterface; `Pick` not only ends up being unhelpful in our documentation system, but it's also of limited help in your IDE. For that reason, avoid `Pick` and other similarly complex types on your public API items. Using these semantics internally is fine. -![pick api documentation](./assets/api_doc_pick.png) +![pick api documentation](../assets/api_doc_pick.png) ### Example plugins @@ -210,7 +210,7 @@ your large change right _after_ feature freeze. If you are worried about missing When possible, build features with incrementals sets of small and focused PRs, but don't check in unused code, and don't expose any feature on master that you would not be comfortable releasing. -![product_stages](./assets/product_stages.png) +![product_stages](../assets/product_stages.png) If your feature cannot be broken down into smaller components, or multiple engineers will be contributing, you have a few other options to consider. diff --git a/dev_docs/dev_principles.mdx b/dev_docs/contributing/dev_principles.mdx similarity index 98% rename from dev_docs/dev_principles.mdx rename to dev_docs/contributing/dev_principles.mdx index 4b238ea24f69..0b8f68d23236 100644 --- a/dev_docs/dev_principles.mdx +++ b/dev_docs/contributing/dev_principles.mdx @@ -1,10 +1,10 @@ --- id: kibDevPrinciples -slug: /kibana-dev-docs/dev-principles +slug: /kibana-dev-docs/contributing/dev-principles title: Developer principles -summary: Follow our development principles to help keep our code base stable, maintainable and scalable. +summary: Follow our development principles to help keep our code base stable, maintainable and scalable. date: 2021-03-04 -tags: ['kibana','onboarding', 'dev', 'architecture'] +tags: ['kibana', 'onboarding', 'dev', 'architecture'] --- Over time, the Kibana project has been shaped by certain principles. Like Kibana itself, some of these principles were formed by intention while others were the result of evolution and circumstance, but today all are important for the continued success and maintainability of Kibana. @@ -117,4 +117,4 @@ The primary consumers of the code we write, the APIs that we create, and the fea Features that we anticipate end users, admins, and plugin developers consuming should be documented through our official docs, but module-level READMEs and code comments are also appropriate. -Documentation is critical part of developing features and code, so an undocumented feature is an incomplete feature. \ No newline at end of file +Documentation is critical part of developing features and code, so an undocumented feature is an incomplete feature. diff --git a/dev_docs/contributing/how_we_use_github.mdx b/dev_docs/contributing/how_we_use_github.mdx index f18bcbcf556f..ff7901fdf08d 100644 --- a/dev_docs/contributing/how_we_use_github.mdx +++ b/dev_docs/contributing/how_we_use_github.mdx @@ -1,6 +1,6 @@ --- id: kibGitHub -slug: /kibana-dev-docs/github +slug: /kibana-dev-docs/contributing/github title: How we use Github summary: Forking, branching, committing and using labels in the Kibana GitHub repo date: 2021-09-16 diff --git a/dev_docs/contributing/standards.mdx b/dev_docs/contributing/standards.mdx index 3d41de8f229c..5f61be80ee20 100644 --- a/dev_docs/contributing/standards.mdx +++ b/dev_docs/contributing/standards.mdx @@ -9,7 +9,7 @@ tags: ['contributor', 'dev', 'github', 'getting started', 'onboarding', 'kibana' ## Developer principles -We expect all developers to read and abide by our overarching . +We expect all developers to read and abide by our overarching . ## Style guide diff --git a/dev_docs/getting_started/add_data.mdx b/dev_docs/getting_started/add_data.mdx index b09e3f6262e7..46822b82fc40 100644 --- a/dev_docs/getting_started/add_data.mdx +++ b/dev_docs/getting_started/add_data.mdx @@ -1,6 +1,6 @@ --- id: kibDevAddData -slug: /kibana-dev-docs/tutorial/sample-data +slug: /kibana-dev-docs/getting-started/sample-data title: Add data summary: Learn how to add data to Kibana date: 2021-08-11 diff --git a/dev_docs/getting_started/dev_welcome.mdx b/dev_docs/getting_started/dev_welcome.mdx index 5e569bd377ee..4080e0850b94 100644 --- a/dev_docs/getting_started/dev_welcome.mdx +++ b/dev_docs/getting_started/dev_welcome.mdx @@ -1,6 +1,6 @@ --- id: kibDevDocsWelcome -slug: /kibana-dev-docs/welcome +slug: /kibana-dev-docs/getting-started/welcome title: Welcome summary: Build custom solutions and applications on top of Kibana. date: 2021-01-02 diff --git a/dev_docs/getting_started/hello_world_plugin.mdx b/dev_docs/getting_started/hello_world_plugin.mdx index 7c02d2807472..b95d0cac201f 100644 --- a/dev_docs/getting_started/hello_world_plugin.mdx +++ b/dev_docs/getting_started/hello_world_plugin.mdx @@ -1,6 +1,6 @@ --- id: kibHelloWorldApp -slug: /kibana-dev-docs/hello-world-app +slug: /kibana-dev-docs/getting-started/hello-world-app title: Hello World summary: Build a very basic plugin that registers an application that says "Hello World!". date: 2021-08-03 diff --git a/dev_docs/getting_started/setting_up_a_development_env.mdx b/dev_docs/getting_started/setting_up_a_development_env.mdx index 04e0511e255b..4338083b1bc8 100644 --- a/dev_docs/getting_started/setting_up_a_development_env.mdx +++ b/dev_docs/getting_started/setting_up_a_development_env.mdx @@ -1,6 +1,6 @@ --- id: kibDevTutorialSetupDevEnv -slug: /kibana-dev-docs/tutorial/setup-dev-env +slug: /kibana-dev-docs/getting-started/setup-dev-env title: Set up a Development Environment summary: Learn how to setup a development environment for contributing to the Kibana repository date: 2021-04-26 diff --git a/dev_docs/troubleshooting.mdx b/dev_docs/getting_started/troubleshooting.mdx similarity index 94% rename from dev_docs/troubleshooting.mdx rename to dev_docs/getting_started/troubleshooting.mdx index f624a8cd7750..e0adfbad86a8 100644 --- a/dev_docs/troubleshooting.mdx +++ b/dev_docs/getting_started/troubleshooting.mdx @@ -1,6 +1,6 @@ --- id: kibTroubleshooting -slug: /kibana-dev-docs/troubleshooting +slug: /kibana-dev-docs/getting-started/troubleshooting title: Troubleshooting summary: A collection of tips for working around strange issues. date: 2021-09-08 diff --git a/dev_docs/key_concepts/anatomy_of_a_plugin.mdx b/dev_docs/key_concepts/anatomy_of_a_plugin.mdx index 3739f907c3d8..ca9119f4d21b 100644 --- a/dev_docs/key_concepts/anatomy_of_a_plugin.mdx +++ b/dev_docs/key_concepts/anatomy_of_a_plugin.mdx @@ -1,6 +1,6 @@ --- id: kibDevAnatomyOfAPlugin -slug: /kibana-dev-docs/anatomy-of-a-plugin +slug: /kibana-dev-docs/key-concepts/anatomy-of-a-plugin title: Anatomy of a plugin summary: Anatomy of a Kibana plugin. date: 2021-08-03 diff --git a/dev_docs/building_blocks.mdx b/dev_docs/key_concepts/building_blocks.mdx similarity index 51% rename from dev_docs/building_blocks.mdx rename to dev_docs/key_concepts/building_blocks.mdx index 6320a7db4558..da3d0f32780b 100644 --- a/dev_docs/building_blocks.mdx +++ b/dev_docs/key_concepts/building_blocks.mdx @@ -1,26 +1,28 @@ --- id: kibBuildingBlocks -slug: /kibana-dev-docs/building-blocks +slug: /kibana-dev-docs/key-concepts/building-blocks title: Building blocks summary: Consider these building blocks when developing your plugin. date: 2021-02-24 -tags: ['kibana','onboarding', 'dev', 'architecture'] +tags: ['kibana', 'onboarding', 'dev', 'architecture'] --- -When building a plugin in Kibana, there are a handful of architectural "building blocks" you can use. Some of these building blocks are "higher-level", -and some are "lower-level". High-level building blocks come +When building a plugin in Kibana, there are a handful of architectural "building blocks" you can use. Some of these building blocks are "higher-level", +and some are "lower-level". High-level building blocks come with many built-in capabilities, require less maintenance, and evolve new feature sets over time with little to no - impact on consumers. When developers use high-level building blocks, new features are exposed consistently, across all of Kibana, at the same time. - On the downside, they are not as flexible as our low-level building blocks. - - Low-level building blocks - provide greater flexibility, but require more code to stitch them together into a meaningful UX. This results in higher maintenance cost for consumers and greater UI/UX variability - across Kibana. - - For example, if an application is using and - , - their application would automatically support runtime fields. If the app is instead using the - lower-level , additional work would be required. +impact on consumers. When developers use high-level building blocks, new features are exposed consistently, across all of Kibana, at the same time. +On the downside, they are not as flexible as our low-level building blocks. + +Low-level building blocks +provide greater flexibility, but require more code to stitch them together into a meaningful UX. This results in higher maintenance cost for consumers and greater UI/UX variability +across Kibana. + +For example, if an application is using and , their application would +automatically support runtime fields. If the app is instead using the lower-level , additional work would be required. Armed with this knowledge, you can choose what works best for your use case! @@ -32,23 +34,15 @@ The following high-level building blocks can be rendered directly into your appl ### Query Bar -The provides a high-level Query Bar component that comes with support for Lucene, KQL, Saved Queries, -and . - -If you would like to expose the ability to search and filter on Elasticsearch data, the Query Bar provided by the - - is your go-to building block. +The provides a high-level Query Bar component that comes with support for Lucene, KQL, Saved Queries, +and . If you would like to expose the ability to search and filter on Elasticsearch data, the Query Bar provided by the is your go-to building block. **Github labels**: `Team:AppServices`, `Feature:QueryBar` ### Dashboard Embeddable Add a Dashboard Embeddable directly inside your application to provide users with a set of visualizations and graphs that work seamlessly -with the . Every feature that is added to a registered - -(Lens, Maps, Saved Searches and more) will be available automatically, as well as any - that are - added to the Embeddable context menu panel (for example, drilldowns, custom panel time ranges, and "share to" features). +with the . Every feature that is added to a registered (Lens, Maps, Saved Searches and more) will be available automatically, as well as any that are added to the Embeddable context menu panel (for example, drilldowns, custom panel time ranges, and "share to" features). The Dashboard Embeddable is one of the highest-level UI components you can add to your application. @@ -56,11 +50,7 @@ The Dashboard Embeddable is one of the highest-level UI components you can add t ### Lens Embeddable -Check out the Lens Embeddable if you wish to show users visualizations based on Elasticsearch data without worrying about query building and chart rendering. It's built on top of the - , and integrates with - - and . Using the same configuration, it's also possible to link to - a prefilled Lens editor, allowing the user to drill deeper and explore their data. +Check out the Lens Embeddable if you wish to show users visualizations based on Elasticsearch data without worrying about query building and chart rendering. It's built on top of the , and integrates with and . Using the same configuration, it's also possible to link to a prefilled Lens editor, allowing the user to drill deeper and explore their data. **Github labels**: `Team:VisEditors`, `Feature:Lens` @@ -72,7 +62,7 @@ Check out the Map Embeddable if you wish to embed a map in your application. ### KibanaPageTemplate -All Kibana pages should use KibanaPageTemplate to setup their pages. It's a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements. +All Kibana pages should use KibanaPageTemplate to setup their pages. It's a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements. Check out for more implementation guidance. @@ -82,10 +72,11 @@ Check out are a high-level, space-aware abstraction layer that sits -above Data Streams and Elasticsearch indices. Index Patterns provide users the -ability to define and customize the data they wish to search and filter on, on a per-space basis. For example, users can specify a set of indices, -and they can customize the field list with runtime fields, formatting options and custom labels. + are a high-level, space-aware +abstraction layer that sits above Data Streams and Elasticsearch indices. Index Patterns provide users +the ability to define and customize the data they wish to search and filter on, on a per-space basis. +For example, users can specify a set of indices, and they can customize the field list with runtime fields, +formatting options and custom labels. Index Patterns are used in many other high-level building blocks so we highly recommend you consider this building block for your search needs. @@ -93,18 +84,23 @@ Index Patterns are used in many other high-level building blocks so we highly re ### Search Source - is a high-level search service offered by the -. It requires an -, and abstracts away the raw ES DSL and search endpoint. Internally -it uses the ES . Use Search Source if you need to query data -from Elasticsearch, and you aren't already using one of the high-level UI Components that handles this internally. + is a high-level search service +offered by the . It requires +an , and abstracts away +the raw ES DSL and search endpoint. Internally it uses the ES +. Use Search Source if you need to query data from Elasticsearch, and you aren't already using one of +the high-level UI Components that handles this internally. **Github labels**: `Team:AppServices`, `Feature:Search` ### Search Strategies -Search Strategies are a low-level building block that abstracts away search details, like what REST endpoint is being called. The ES Search Strategy -is a very lightweight abstraction layer that sits just above querying ES with the elasticsearch-js client. Other search stragies are offered for other +Search Strategies are a low-level building block that abstracts away search details, like what REST endpoint is being called. The ES Search Strategy +is a very lightweight abstraction layer that sits just above querying ES with the elasticsearch-js client. Other search stragies are offered for other languages, like EQL and SQL. These are very low-level building blocks so expect a lot of glue work to make these work with the higher-level abstractions. **Github labels**: `Team:AppServices`, `Feature:Search` @@ -112,24 +108,24 @@ languages, like EQL and SQL. These are very low-level building blocks so expect ### Expressions Expressions are a low-level building block that can be used if you have advanced search needs that requiring piping results into additional functionality, like -joining and manipulating data. Lens and Canvas are built on top of Expressions. Most developers should be able to use - or - , rather than need to access the Expression language directly. +joining and manipulating data. Lens and Canvas are built on top of Expressions. Most developers should be able to use or , rather than need to +access the Expression language directly.{' '} **Github labels**: `Team:AppServices`, `Feature:ExpressionLanguage` ## Saved Objects - should be used if you need to persist application-level information. If you were building a TODO -application, each TODO item would be a `Saved Object`. Saved objects come pre-wired with support for bulk export/import, security features like space sharing and -space isolation, and tags. + should be used if you need to persist +application-level information. If you were building a TODO application, each TODO item would be a `Saved +Object`. Saved objects come pre-wired with support for bulk export/import, security features like space +sharing and space isolation, and tags. **Github labels**: `Team:Core`, `Feature:Saved Objects` # Integration building blocks Use the following building blocks to create an inter-connected, cross-application, holistic Kibana experience. These building blocks allow you to expose functionality - that promotes your own application into other applications, as well as help developers of other applications integrate into your app. +that promotes your own application into other applications, as well as help developers of other applications integrate into your app. ## UI Actions & Triggers @@ -141,6 +137,6 @@ application could register a UI Action called "View in Maps" to appear any time ## Embeddables Embeddables help you integrate your application with the Dashboard application. Register your custom UI Widget as an Embeddable and users will -be able to add it as a panel on a Dashboard. With a little extra work, it can also be exposed in Canvas workpads. +be able to add it as a panel on a Dashboard. With a little extra work, it can also be exposed in Canvas workpads. **Github labels**: `Team:AppServices`, `Feature:Embeddables` diff --git a/dev_docs/key_concepts/data_views.mdx b/dev_docs/key_concepts/data_views.mdx index e2b64c8705c4..c514af21c0cf 100644 --- a/dev_docs/key_concepts/data_views.mdx +++ b/dev_docs/key_concepts/data_views.mdx @@ -1,16 +1,16 @@ --- id: kibDataViewsKeyConcepts -slug: /kibana-dev-docs/data-view-intro +slug: /kibana-dev-docs/key-concepts/data-view-intro title: Data Views summary: Data views are the central method of defining queryable data sets in Kibana date: 2021-08-11 -tags: ['kibana','dev', 'contributor', 'api docs'] +tags: ['kibana', 'dev', 'contributor', 'api docs'] --- -*Note: Kibana index patterns are currently being renamed to data views. There will be some naming inconsistencies until the transition is complete.* +_Note: Kibana index patterns are currently being renamed to data views. There will be some naming inconsistencies until the transition is complete._ Data views (formerly Kibana index patterns or KIPs) are the central method of describing sets of indices for queries. Usage is strongly recommended -as a number of high level rely on them. Further, they provide a consistent view of data across +as a number of high level rely on them. Further, they provide a consistent view of data across a variety Kibana apps. Data views are defined by a wildcard string (an index pattern) which matches indices, data streams, and index aliases, optionally specify a @@ -20,8 +20,6 @@ on the data view via runtime fields. Schema-on-read functionality is provided by ![image](../assets/data_view_diagram.png) - - The data view API is made available via the data plugin (`data.indexPatterns`, soon to be renamed) and most commonly used with (`data.search.search.SearchSource`) to perform queries. SearchSource will apply existing filters and queries from the search bar UI. @@ -29,4 +27,3 @@ Users can create data views via [Data view management](https://www.elastic.co/gu Additionally, they can be created through the data view API. Data views also allow formatters and custom labels to be defined for fields. - diff --git a/dev_docs/kibana_platform_plugin_intro.mdx b/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx similarity index 99% rename from dev_docs/kibana_platform_plugin_intro.mdx rename to dev_docs/key_concepts/kibana_platform_plugin_intro.mdx index 252a6dcd9cd8..b2255dbc8e5c 100644 --- a/dev_docs/kibana_platform_plugin_intro.mdx +++ b/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx @@ -1,6 +1,6 @@ --- id: kibPlatformIntro -slug: /kibana-dev-docs/platform-intro +slug: /kibana-dev-docs/key-concepts/platform-intro title: Plugins and the Kibana platform summary: An introduction to the Kibana platform and how to use it to build a plugin. date: 2021-01-06 diff --git a/dev_docs/key_concepts/performance.mdx b/dev_docs/key_concepts/performance.mdx index 2870262825e4..0201c7774f85 100644 --- a/dev_docs/key_concepts/performance.mdx +++ b/dev_docs/key_concepts/performance.mdx @@ -1,6 +1,6 @@ --- id: kibDevPerformance -slug: /kibana-dev-docs/performance +slug: /kibana-dev-docs/key-concepts/performance title: Performance summary: Performance tips for Kibana development. date: 2021-09-02 @@ -9,13 +9,13 @@ tags: ['kibana', 'onboarding', 'dev', 'performance'] ## Keep Kibana fast -*tl;dr*: Load as much code lazily as possible. Everyone loves snappy +_tl;dr_: Load as much code lazily as possible. Everyone loves snappy applications with a responsive UI and hates spinners. Users deserve the best experience whether they run Kibana locally or in the cloud, regardless of their hardware and environment. There are 2 main aspects of the perceived speed of an application: loading time -and responsiveness to user actions. Kibana loads and bootstraps *all* +and responsiveness to user actions. Kibana loads and bootstraps _all_ the plugins whenever a user lands on any page. It means that every new application affects the overall _loading performance_, as plugin code is loaded _eagerly_ to initialize the plugin and provide plugin API to dependent @@ -60,12 +60,12 @@ export class MyPlugin implements Plugin { ### Understanding plugin bundle size -Kibana Platform plugins are pre-built with `@kbn/optimizer` +Kibana Platform plugins are pre-built with `@kbn/optimizer` and distributed as package artifacts. This means that it is no -longer necessary for us to include the `optimizer` in the +longer necessary for us to include the `optimizer` in the distributable version of Kibana Every plugin artifact contains all plugin dependencies required to run the plugin, except some -stateful dependencies shared across plugin bundles via +stateful dependencies shared across plugin bundles via `@kbn/ui-shared-deps-npm` and `@kbn/ui-shared-deps-src`. This means that plugin artifacts _tend to be larger_ than they were in the legacy platform. To understand the current size of your plugin @@ -101,7 +101,7 @@ node scripts/build_kibana_platform_plugins.js --dist --no-examples --profile Many OSS tools allow you to analyze the generated stats file: -* [An official tool](https://webpack.github.io/analyse/#modules) from -Webpack authors -* [webpack-visualizer](https://chrisbateman.github.io/webpack-visualizer/) -* [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) +- [An official tool](https://webpack.github.io/analyse/#modules) from + Webpack authors +- [webpack-visualizer](https://chrisbateman.github.io/webpack-visualizer/) +- [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) diff --git a/dev_docs/key_concepts/persistable_state.mdx b/dev_docs/key_concepts/persistable_state.mdx index 4368417170ee..189259cf1085 100644 --- a/dev_docs/key_concepts/persistable_state.mdx +++ b/dev_docs/key_concepts/persistable_state.mdx @@ -1,19 +1,19 @@ --- id: kibDevDocsPersistableStateIntro -slug: /kibana-dev-docs/persistable-state-intro +slug: /kibana-dev-docs/key-concepts/persistable-state-intro title: Persistable State summary: Persitable state is a key concept to understand when building a Kibana plugin. date: 2021-02-02 -tags: ['kibana','dev', 'contributor', 'api docs'] +tags: ['kibana', 'dev', 'contributor', 'api docs'] --- - “Persistable state” is developer-defined state that supports being persisted by a plugin other than the one defining it. Persistable State needs to be serializable and the owner can/should provide utilities to migrate it, extract and inject any it may contain, as well as telemetry collection utilities. - +“Persistable state” is developer-defined state that supports being persisted by a plugin other than the one defining it. Persistable State needs to be serializable and the owner can/should provide utilities to migrate it, extract and inject any it may contain, as well as telemetry collection utilities. + ## Exposing state that can be persisted Any plugin that exposes state that another plugin might persist should implement interface on their `setup` contract. This will allow plugins persisting the state to easily access migrations and other utilities. -Example: Data plugin allows you to generate filters. Those filters can be persisted by applications in their saved +Example: Data plugin allows you to generate filters. Those filters can be persisted by applications in their saved objects or in the URL. In order to allow apps to migrate the filters in case the structure changes in the future, the Data plugin implements `PersistableStateService` on . note: There is currently no obvious way for a plugin to know which state is safe to persist. The developer must manually look for a matching `PersistableStateService`, or ad-hoc provided migration utilities (as is the case with Rule Type Parameters). @@ -26,9 +26,10 @@ interface on their `setup` contract and each item in the collection should imple Example: Embeddable plugin owns the registry of embeddable factories to which other plugins can register new embeddable factories. Dashboard plugin stores a bunch of embeddable panels input in its saved object and URL. Embeddable plugin setup contract implements `PersistableStateService` -interface and each `EmbeddableFactory` needs to implement `PersistableStateDefinition` interface. +interface and each `EmbeddableFactory` needs to implement `PersistableStateDefinition` interface. Embeddable plugin exposes this interfaces: + ``` // EmbeddableInput implements Serializable @@ -45,7 +46,7 @@ If the state your plugin is storing can be provided by other plugins (your plugi ## Storing persistable state as part of saved object -Any plugin that stores any persistable state as part of their saved object should make sure that its saved object migration +Any plugin that stores any persistable state as part of their saved object should make sure that its saved object migration and reference extraction and injection methods correctly use the matching `PersistableStateService` implementation for the state they are storing. Take a look at [example saved object](https://github.com/elastic/kibana/blob/master/examples/embeddable_examples/server/searchable_list_saved_object.ts#L32) which stores an embeddable state. Note how the `migrations`, `extractReferences` and `injectReferences` are defined. @@ -58,13 +59,17 @@ of `PersistableStateService` should be called, which will migrate the state from note: Currently there is no recommended way on how to store version in url and its up to every application to decide on how to implement that. ## Available state operations - + ### Extraction/Injection of References In order to support import and export, and space-sharing capabilities, Saved Objects need to explicitly list any references they contain to other Saved Objects. To support persisting your state in saved objects owned by another plugin, the and methods of Persistable State interface should be implemented. - + [See example embeddable providing extract/inject functions](https://github.com/elastic/kibana/blob/master/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts) @@ -72,7 +77,7 @@ To support persisting your state in saved objects owned by another plugin, the < As your plugin evolves, you may need to change your state in a breaking way. If that happens, you should write a migration to upgrade the state that existed prior to the change. -. +. [See an example saved object storing embeddable state implementing saved object migration function](https://github.com/elastic/kibana/blob/master/examples/embeddable_examples/server/searchable_list_saved_object.ts) @@ -80,4 +85,4 @@ As your plugin evolves, you may need to change your state in a breaking way. If ## Telemetry -You might want to collect statistics about how your state is used. If that is the case you should implement the telemetry method of Persistable State interface. +You might want to collect statistics about how your state is used. If that is the case you should implement the telemetry method of Persistable State interface. diff --git a/dev_docs/key_concepts/saved_objects.mdx b/dev_docs/key_concepts/saved_objects.mdx index bef92bf02869..7fe66b9eab95 100644 --- a/dev_docs/key_concepts/saved_objects.mdx +++ b/dev_docs/key_concepts/saved_objects.mdx @@ -1,39 +1,42 @@ --- id: kibDevDocsSavedObjectsIntro -slug: /kibana-dev-docs/saved-objects-intro +slug: /kibana-dev-docs/key-concepts/saved-objects-intro title: Saved Objects summary: Saved Objects are a key concept to understand when building a Kibana plugin. date: 2021-02-02 -tags: ['kibana','dev', 'contributor', 'api docs'] +tags: ['kibana', 'dev', 'contributor', 'api docs'] --- "Saved Objects" are developer defined, persisted entities, stored in the Kibana system index (which is also sometimes referred to as the `.kibana` index). The Saved Objects service allows Kibana plugins to use Elasticsearch like a primary database. Think of it as an Object Document Mapper for Elasticsearch. - Some examples of Saved Object types are dashboards, lens, canvas workpads, index patterns, cases, ml jobs, and advanced settings. Some Saved Object types are - exposed to the user in the [Saved Object management UI](https://www.elastic.co/guide/en/kibana/current/managing-saved-objects.html), but not all. +Some examples of Saved Object types are dashboards, lens, canvas workpads, index patterns, cases, ml jobs, and advanced settings. Some Saved Object types are +exposed to the user in the [Saved Object management UI](https://www.elastic.co/guide/en/kibana/current/managing-saved-objects.html), but not all. Developers create and manage their Saved Objects using the SavedObjectClient, while other data in Elasticsearch should be accessed via the data plugin's search services. ![image](../assets/saved_object_vs_data_indices.png) - - + ## References In order to support import and export, and space-sharing capabilities, Saved Objects need to explicitly list any references they contain to other Saved Objects. The parent should have a reference to it's children, not the other way around. That way when a "parent" is exported (or shared to a space), - all the "children" will be automatically included. However, when a "child" is exported, it will not include all "parents". +all the "children" will be automatically included. However, when a "child" is exported, it will not include all "parents". - + ## Migrations and Backward compatibility As your plugin evolves, you may need to change your Saved Object type in a breaking way (for example, changing the type of an attribtue, or removing an attribute). If that happens, you should write a migration to upgrade the Saved Objects that existed prior to the change. -. +. ## Security @@ -47,30 +50,30 @@ Saved Objects are "space aware". They exist in the space they were created in, a Feature controls provide another level of isolation and shareability for Saved Objects. Admins can give users and roles read, write or none permissions for each Saved Object type. -### Object level security (OLS) +### Object level security (OLS) OLS is an oft-requested feature that is not implemented yet. When it is, it will provide users with even more sharing and privacy flexibility. Individual objects can be private to the user, shared with a selection of others, or made public. Much like how sharing Google Docs works. - + ## Scalability By default all saved object types go into a single index. If you expect your saved object type to have a lot of unique fields, or if you expect there -to be many of them, you can have your objects go in a separate index by using the `indexPattern` field. Reporting and task manager are two +to be many of them, you can have your objects go in a separate index by using the `indexPattern` field. Reporting and task manager are two examples of features that use this capability. ## Searchability -Because saved objects are stored in system indices, they cannot be searched like other data can. If you see the phrase “[X] as data” it is +Because saved objects are stored in system indices, they cannot be searched like other data can. If you see the phrase “[X] as data” it is referring to this searching limitation. Users will not be able to create custom dashboards using saved object data, like they would for data stored in Elasticsearch data indices. ## Saved Objects by value Sometimes Saved Objects end up persisted inside another Saved Object. We call these Saved Objects “by value”, as opposed to "by - reference". If an end user creates a visualization and adds it to a dashboard without saving it to the visualization - library, the data ends up nested inside the dashboard Saved Object. This helps keep the visualization library smaller. It also avoids - issues with edits propagating - since an entity can only exist in a single place. - Note that from the end user stand point, we don’t use these terms “by reference” and “by value”. +reference". If an end user creates a visualization and adds it to a dashboard without saving it to the visualization +library, the data ends up nested inside the dashboard Saved Object. This helps keep the visualization library smaller. It also avoids +issues with edits propagating - since an entity can only exist in a single place. +Note that from the end user stand point, we don’t use these terms “by reference” and “by value”. ## Sharing Saved Objects @@ -80,7 +83,7 @@ on how it is registered. If you are adding a **new** object type, when you register it: 1. Use `namespaceType: 'multiple-isolated'` to make these objects exist in exactly one space -2. Use `namespaceType: 'multiple'` to make these objects exist in one *or more* spaces +2. Use `namespaceType: 'multiple'` to make these objects exist in one _or more_ spaces 3. Use `namespaceType: 'agnostic'` if you want these objects to always exist in all spaces If you have an **existing** "legacy" object type that is not shareable (using `namespaceType: 'single'`), see the [legacy developer guide diff --git a/dev_docs/tutorials/building_a_kibana_distributable.mdx b/dev_docs/tutorials/building_a_kibana_distributable.mdx index 7b06525a5b97..e73481058ab3 100644 --- a/dev_docs/tutorials/building_a_kibana_distributable.mdx +++ b/dev_docs/tutorials/building_a_kibana_distributable.mdx @@ -1,6 +1,6 @@ --- id: kibDevTutorialBuildingDistributable -slug: /kibana-dev-docs/tutorial/building-distributable +slug: /kibana-dev-docs/tutorials/building-distributable title: Building a Kibana distributable summary: Learn how to build a Kibana distributable date: 2021-05-10 diff --git a/dev_docs/tutorials/data/search.mdx b/dev_docs/tutorials/data/search.mdx index 9cf46bb96c72..81080b0c2741 100644 --- a/dev_docs/tutorials/data/search.mdx +++ b/dev_docs/tutorials/data/search.mdx @@ -2,14 +2,14 @@ id: kibDevTutorialDataSearchAndSessions slug: /kibana-dev-docs/tutorials/data/search-and-sessions title: Kibana data.search Services -summary: Kibana Search Services +summary: Kibana Search Services date: 2021-02-10 tags: ['kibana', 'onboarding', 'dev', 'tutorials', 'search', 'sessions', 'search-sessions'] --- ## Search service -### Low level search +### Low level search Searching data stored in Elasticsearch can be done in various ways, for example using the Elasticsearch REST API or using an `Elasticsearch Client` for low level access. @@ -50,7 +50,7 @@ export class MyPlugin implements Plugin { } else { // handle partial results if you want. } - }, + }, error: (e) => { // handle error thrown, for example a server hangup }, @@ -69,36 +69,36 @@ Note: The `data` plugin contains services to help you generate the `query` and ` The `search` method can throw several types of errors, for example: - - `EsError` for errors originating in Elasticsearch errors - - `PainlessError` for errors originating from a Painless script - - `AbortError` if the search was aborted via an `AbortController` - - `HttpError` in case of a network error +- `EsError` for errors originating in Elasticsearch errors +- `PainlessError` for errors originating from a Painless script +- `AbortError` if the search was aborted via an `AbortController` +- `HttpError` in case of a network error -To display the errors in the context of an application, use the helper method provided on the `data.search` service. These errors are shown in a toast message, using the `core.notifications` service. +To display the errors in the context of an application, use the helper method provided on the `data.search` service. These errors are shown in a toast message, using the `core.notifications` service. ```ts data.search.search(req).subscribe({ - next: (result) => {}, + next: (result) => {}, error: (e) => { data.search.showError(e); }, -}) +}); ``` If you decide to handle errors by yourself, watch for errors coming from `Elasticsearch`. They have an additional `attributes` property that holds the raw error from `Elasticsearch`. ```ts data.search.search(req).subscribe({ - next: (result) => {}, + next: (result) => {}, error: (e) => { if (e instanceof IEsError) { showErrorReason(e.attributes); } }, -}) +}); ``` -#### Stop a running search +#### Stop a running search The search service `search` method supports a second argument called `options`. One of these options provides an `abortSignal` to stop searches from running to completion, if the result is no longer needed. @@ -106,20 +106,22 @@ The search service `search` method supports a second argument called `options`. import { AbortError } from '../../src/data/public'; const abortController = new AbortController(); -data.search.search(req, { - abortSignal: abortController.signal, -}).subscribe({ - next: (result) => { - // handle result - }, - error: (e) => { - if (e instanceof AbortError) { - // you can ignore this error - return; - } - // handle error, for example a server hangup - }, -}); +data.search + .search(req, { + abortSignal: abortController.signal, + }) + .subscribe({ + next: (result) => { + // handle result + }, + error: (e) => { + if (e instanceof AbortError) { + // you can ignore this error + return; + } + // handle error, for example a server hangup + }, + }); // Abort the search request after a second setTimeout(() => { @@ -135,13 +137,15 @@ For example, to run an EQL query using the `data.search` service, you should to ```ts const req = getEqlRequest(); -data.search.search(req, { - strategy: EQL_SEARCH_STRATEGY, -}).subscribe({ - next: (result) => { - // handle EQL result - }, -}); +data.search + .search(req, { + strategy: EQL_SEARCH_STRATEGY, + }) + .subscribe({ + next: (result) => { + // handle EQL result + }, + }); ``` ##### Custom search strategies @@ -154,18 +158,18 @@ The following example shows how to define, register, and use a search strategy t // ./myPlugin/server/myStrategy.ts /** - * Your custom search strategy should implement the ISearchStrategy interface, requiring at minimum a `search` function. + * Your custom search strategy should implement the ISearchStrategy interface, requiring at minimum a `search` function. */ export const mySearchStrategyProvider = ( data: PluginStart ): ISearchStrategy => { const preprocessRequest = (request: IMyStrategyRequest) => { // Custom preprocessing - } + }; const formatResponse = (response: IMyStrategyResponse) => { // Custom post-processing - } + }; // Get the default search strategy const es = data.search.getSearchStrategy(ES_SEARCH_STRATEGY); @@ -179,16 +183,12 @@ export const mySearchStrategyProvider = ( ```ts // ./myPlugin/server/plugin.ts -import type { - CoreSetup, - CoreStart, - Plugin, -} from 'kibana/server'; +import type { CoreSetup, CoreStart, Plugin } from 'kibana/server'; import { mySearchStrategyProvider } from './my_strategy'; /** - * Your plugin will receive the `data` plugin contact in both the setup and start lifecycle hooks. + * Your plugin will receive the `data` plugin contact in both the setup and start lifecycle hooks. */ export interface MyPluginSetupDeps { data: PluginSetup; @@ -199,13 +199,10 @@ export interface MyPluginStartDeps { } /** - * In your custom server side plugin, register the strategy from the setup contract + * In your custom server side plugin, register the strategy from the setup contract */ export class MyPlugin implements Plugin { - public setup( - core: CoreSetup, - deps: MyPluginSetupDeps - ) { + public setup(core: CoreSetup, deps: MyPluginSetupDeps) { core.getStartServices().then(([_, depsStart]) => { const myStrategy = mySearchStrategyProvider(depsStart.data); deps.data.search.registerSearchStrategy('myCustomStrategy', myStrategy); @@ -217,13 +214,15 @@ export class MyPlugin implements Plugin { ```ts // ./myPlugin/public/plugin.ts const req = getRequest(); -data.search.search(req, { - strategy: 'myCustomStrategy', -}).subscribe({ - next: (result) => { - // handle result - }, -}); +data.search + .search(req, { + strategy: 'myCustomStrategy', + }) + .subscribe({ + next: (result) => { + // handle result + }, + }); ``` ##### Async search and custom async search strategies @@ -234,7 +233,7 @@ This synchronous execution works great in most cases. However, with the introduc The [async_search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/async-search.html) is what drives more advanced `Kibana` `search` features, such as `partial results` and `search sessions`. [When available](https://www.elastic.co/subscriptions), the default search strategy of `Kibana` is automatically set to the **async** default search strategy (`ENHANCED_ES_SEARCH_STRATEGY`), empowering Kibana to run longer queries, with an **optional** duration restriction defined by the UI setting `search:timeout`. -If you are implementing your own async custom search strategy, make sure to implement `cancel` and `extend`, as shown in the following example: +If you are implementing your own async custom search strategy, make sure to implement `cancel` and `extend`, as shown in the following example: ```ts // ./myPlugin/server/myEnhancedStrategy.ts @@ -245,7 +244,7 @@ export const myEnhancedSearchStrategyProvider = ( const ese = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); return { search: (request, options, deps) => { - // search will be called multiple times, + // search will be called multiple times, // be sure your response formatting is capable of handling partial results, as well as the final result. return formatResponse(ese.search(request, options, deps)); }, @@ -286,7 +285,7 @@ function searchWithSearchSource() { .setField('query', query) .setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : ['*']) .setField('aggs', getAggsDsl()); - + searchSource.fetch$().subscribe({ next: () => {}, error: () => {}, @@ -296,7 +295,7 @@ function searchWithSearchSource() { ### Partial results -When searching using an `async` strategy (such as async DSL and async EQL), the search service will stream back partial results. +When searching using an `async` strategy (such as async DSL and async EQL), the search service will stream back partial results. Although you can ignore the partial results and wait for the final result before rendering, you can also use the partial results to create a more interactive experience for your users. It is highly advised, however, to make sure users are aware that the results they are seeing are partial. @@ -310,7 +309,7 @@ data.search.search(req).subscribe({ renderPartialResult(res); } }, -}) +}); // Skipping partial results const finalResult = await data.search.search(req).toPromise(); @@ -320,31 +319,29 @@ const finalResult = await data.search.search(req).toPromise(); A search session is a higher level concept than search. A search session describes a grouping of one or more async search requests with additional context. -Search sessions are handy when you want to enable a user to run something asynchronously (for example, a dashboard over a long period of time), and then quickly restore the results at a later time. The `Search Service` transparently fetches results from the `.async-search` index, instead of running each request again. +Search sessions are handy when you want to enable a user to run something asynchronously (for example, a dashboard over a long period of time), and then quickly restore the results at a later time. The `Search Service` transparently fetches results from the `.async-search` index, instead of running each request again. Internally, any search run within a search session is saved into an object, allowing Kibana to manage their lifecycle. Most saved objects are deleted automatically after a short period of time, but if a user chooses to save the search session, the saved object is persisted, so that results can be restored in a later time. -Stored search sessions are listed in the *Management* application, under *Kibana > Search Sessions*, making it easy to find, manage, and restore them. +Stored search sessions are listed in the _Management_ application, under _Kibana > Search Sessions_, making it easy to find, manage, and restore them. As a developer, you might encounter these two common, use cases: - * Running a search inside an existing search session - * Supporting search sessions in your application +- Running a search inside an existing search session +- Supporting search sessions in your application #### Running a search inside an existing search session For this example, assume you are implementing a new type of `Embeddable` that will be shown on dashboards. The same principle applies, however, to any search requests that you are running, as long as the application you are running inside is managing an active session. -Because the Dashboard application is already managing a search session, all you need to do is pass down the `searchSessionId` argument to any `search` call. This applies to both the low and high level search APIs. +Because the Dashboard application is already managing a search session, all you need to do is pass down the `searchSessionId` argument to any `search` call. This applies to both the low and high level search APIs. -The search information will be added to the saved object for the search session. +The search information will be added to the saved object for the search session. ```ts -export class SearchEmbeddable - extends Embeddable { - +export class SearchEmbeddable extends Embeddable { private async fetchData() { - // Every embeddable receives an optional `searchSessionId` input parameter. + // Every embeddable receives an optional `searchSessionId` input parameter. const { searchSessionId } = this.input; // Setup your search source @@ -355,9 +352,11 @@ export class SearchEmbeddable this.updateOutput({ loading: true, error: undefined }); // Make the request, wait for the final result - const {rawResponse: resp} = await searchSource.fetch$({ - sessionId: searchSessionId, - }).toPromise(); + const { rawResponse: resp } = await searchSource + .fetch$({ + sessionId: searchSessionId, + }) + .toPromise(); this.useSearchResult(resp); @@ -368,35 +367,37 @@ export class SearchEmbeddable } } } - -``` +``` You can also retrieve the active `Search Session ID` from the `Search Service` directly: ```ts async function fetchData(data: DataPublicPluginStart) { try { - return await searchSource.fetch$({ + return await searchSource + .fetch$({ sessionId: data.search.sessions.getSessionId(), - }).toPromise(); + }) + .toPromise(); } catch (e) { // handle search errors } } - ``` - Search sessions are initiated by the client. If you are using a route that runs server side searches, you can send the `searchSessionId` to the server, and then pass it down to the server side `data.search` function call. + Search sessions are initiated by the client. If you are using a route that runs server side + searches, you can send the `searchSessionId` to the server, and then pass it down to the server + side `data.search` function call. #### Supporting search sessions in your application Before implementing the ability to create and restore search sessions in your application, ask yourself the following questions: -1. **Does your application normally run long operations?** For example, it makes sense for a user to generate a Dashboard or a Canvas report from data stored in cold storage. However, when editing a single visualization, it is best to work with a shorter timeframe of hot or warm data. +1. **Does your application normally run long operations?** For example, it makes sense for a user to generate a Dashboard or a Canvas report from data stored in cold storage. However, when editing a single visualization, it is best to work with a shorter timeframe of hot or warm data. 2. **Does it make sense for your application to restore a search session?** For example, you might want to restore an interesting configuration of filters of older documents you found in Discover. However, a single Lens or Map visualization might not be as helpful, outside the context of a specific dashboard. -3. **What is a search session in the context of your application?** Although Discover and Dashboard start a new search session every time the time range or filters change, or when the user clicks **Refresh**, you can manage your sessions differently. For example, if your application has tabs, you might group searches from multiple tabs into a single search session. You must be able to clearly define the **state** used to create the search session`. The **state** refers to any setting that might change the queries being set to `Elasticsearch`. +3. **What is a search session in the context of your application?** Although Discover and Dashboard start a new search session every time the time range or filters change, or when the user clicks **Refresh**, you can manage your sessions differently. For example, if your application has tabs, you might group searches from multiple tabs into a single search session. You must be able to clearly define the **state** used to create the search session`. The **state** refers to any setting that might change the queries being set to `Elasticsearch`. Once you answer those questions, proceed to implement the following bits of code in your application. @@ -409,8 +410,8 @@ export class MyPlugin implements Plugin { public start(core: CoreStart, { data }: MyPluginStartDependencies) { const sessionRestorationDataProvider: SearchSessionInfoProvider = { data, - getDashboard - } + getDashboard, + }; data.search.session.enableStorage({ getName: async () => { @@ -430,32 +431,34 @@ export class MyPlugin implements Plugin { ``` - The restore state of a search session may be different from the initial state used to create it. For example, where the initial state may contain relative dates, in the restore state, those must be converted to absolute dates. Read more about the [NowProvider](). + The restore state of a search session may be different from the initial state used to create it. + For example, where the initial state may contain relative dates, in the restore state, those must + be converted to absolute dates. Read more about the [NowProvider](). - Calling `enableStorage` will also enable the `Search Session Indicator` component in the chrome component of your solution. The `Search Session Indicator` is a small button, used by default to engage users and save new search sessions. To implement your own UI, contact the Kibana application services team to decouple this behavior. + Calling `enableStorage` will also enable the `Search Session Indicator` component in the chrome + component of your solution. The `Search Session Indicator` is a small button, used by default to + engage users and save new search sessions. To implement your own UI, contact the Kibana + application services team to decouple this behavior. -##### Start a new search session +##### Start a new search session -Make sure to call `start` when the **state** you previously defined changes. +Make sure to call `start` when the **state** you previously defined changes. ```ts - function onSearchSessionConfigChange() { this.searchSessionId = data.search.sessions.start(); } - ``` -Pass the `searchSessionId` to every `search` call inside your application. If you're using `Embeddables`, pass down the `searchSessionId` as `input`. +Pass the `searchSessionId` to every `search` call inside your application. If you're using `Embeddables`, pass down the `searchSessionId` as `input`. If you can't pass the `searchSessionId` directly, you can retrieve it from the service. ```ts const currentSearchSessionId = data.search.sessions.getSessionId(); - ``` ##### Clear search sessions @@ -466,19 +469,17 @@ Creating a new search session clears the previous one. You must explicitly `clea function onDestroy() { data.search.session.clear(); } - ``` If you don't call `clear`, you will see a warning in the console while developing. However, when running in production, you will get a fatal error. This is done to avoid leakage of unrelated search requests into an existing search session left open by mistake. -##### Restore search sessions +##### Restore search sessions -The last step of the integration is restoring an existing search session. The `searchSessionId` parameter and the rest of the restore state are passed into the application via the URL. Non-URL support is planned for future releases. +The last step of the integration is restoring an existing search session. The `searchSessionId` parameter and the rest of the restore state are passed into the application via the URL. Non-URL support is planned for future releases. If you detect the presense of a `searchSessionId` parameter in the URL, call the `restore` method **instead** of calling `start`. The previous example would now become: ```ts - function onSearchSessionConfigChange(searchSessionIdFromUrl?: string) { if (searchSessionIdFromUrl) { data.search.sessions.restore(searchSessionIdFromUrl); @@ -486,7 +487,6 @@ function onSearchSessionConfigChange(searchSessionIdFromUrl?: string) { data.search.sessions.start(); } } - ``` Once you `restore` the session, as long as all `search` requests run with the same `searchSessionId`, the search session should be seamlessly restored. diff --git a/dev_docs/tutorials/debugging.mdx b/dev_docs/tutorials/debugging.mdx index c0efd249be06..c612893e4f1f 100644 --- a/dev_docs/tutorials/debugging.mdx +++ b/dev_docs/tutorials/debugging.mdx @@ -1,6 +1,6 @@ --- id: kibDevTutorialDebugging -slug: /kibana-dev-docs/tutorial/debugging +slug: /kibana-dev-docs/tutorials/debugging title: Debugging in development summary: Learn how to debug Kibana while running from source date: 2021-04-26 @@ -27,7 +27,7 @@ You will need to run Jest directly from the Node script: `node --inspect-brk scripts/functional_test_runner` -### Development Server +### Development Server `node --inspect-brk scripts/kibana` @@ -58,4 +58,4 @@ logging: level: debug - name: elasticsearch.query level: debug -``` \ No newline at end of file +``` diff --git a/dev_docs/tutorials/saved_objects.mdx b/dev_docs/tutorials/saved_objects.mdx index 35efbb97a0a0..29a0b60983d9 100644 --- a/dev_docs/tutorials/saved_objects.mdx +++ b/dev_docs/tutorials/saved_objects.mdx @@ -1,6 +1,6 @@ --- id: kibDevTutorialSavedObject -slug: /kibana-dev-docs/tutorial/saved-objects +slug: /kibana-dev-docs/tutorials/saved-objects title: Register a new saved object type summary: Learn how to register a new saved object type. date: 2021-02-05 diff --git a/dev_docs/tutorials/submit_a_pull_request.mdx b/dev_docs/tutorials/submit_a_pull_request.mdx index 2be5973bb385..5436ebf24e03 100644 --- a/dev_docs/tutorials/submit_a_pull_request.mdx +++ b/dev_docs/tutorials/submit_a_pull_request.mdx @@ -1,6 +1,6 @@ --- id: kibDevTutorialSubmitPullRequest -slug: /kibana-dev-docs/tutorial/submit-pull-request +slug: /kibana-dev-docs/tutorials/submit-pull-request title: Submitting a Kibana pull request summary: Learn how to submit a Kibana pull request date: 2021-06-24 diff --git a/dev_docs/tutorials/testing_plugins.mdx b/dev_docs/tutorials/testing_plugins.mdx index bc92af33d349..14089bc3fa31 100644 --- a/dev_docs/tutorials/testing_plugins.mdx +++ b/dev_docs/tutorials/testing_plugins.mdx @@ -1,6 +1,6 @@ --- id: kibDevTutorialTestingPlugins -slug: /kibana-dev-docs/tutorial/testing-plugins +slug: /kibana-dev-docs/tutorials/testing-plugins title: Testing Kibana Plugins summary: Learn how to test different aspects of Kibana plugins date: 2021-07-05 diff --git a/docs/developer/architecture/core/logging-configuration-migration.asciidoc b/docs/developer/architecture/core/logging-configuration-migration.asciidoc index 19f10a881d5e..db02b4d4e507 100644 --- a/docs/developer/architecture/core/logging-configuration-migration.asciidoc +++ b/docs/developer/architecture/core/logging-configuration-migration.asciidoc @@ -76,9 +76,5 @@ you can override the flags with: |--verbose| --logging.root.level=debug --logging.root.appenders[0]=default --logging.root.appenders[1]=custom | --verbose -|--quiet| --logging.root.level=error --logging.root.appenders[0]=default --logging.root.appenders[1]=custom | not supported - |--silent| --logging.root.level=off | --silent |=== - -NOTE: To preserve backwards compatibility, you are required to pass the root `default` appender until the legacy logging system is removed in `v8.0`. diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index b42bc980c875..775446333977 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -74,7 +74,6 @@ yarn kbn watch - @kbn/i18n - @kbn/interpreter - @kbn/io-ts-utils -- @kbn/legacy-logging - @kbn/logging - @kbn/mapbox-gl - @kbn/monaco diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index edc1821f3b22..7f7041f7815c 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -57,6 +57,12 @@ as uiSettings within the code. |The data plugin provides common data access services, such as search and query, for solutions and application developers. +|{kib-repo}blob/{branch}/src/plugins/data_views/README.mdx[dataViews] +|The data views API provides a consistent method of structuring and formatting documents +and field lists across the various Kibana apps. Its typically used in conjunction with + for composing queries. + + |{kib-repo}blob/{branch}/src/plugins/dev_tools/README.md[devTools] |The ui/registry/dev_tools is removed in favor of the devTools plugin which exposes a register method in the setup contract. Registering app works mostly the same as registering apps in core.application.register. diff --git a/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md b/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md index f6de959589ec..7d9772af91c3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md +++ b/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig; +export declare type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig; ``` diff --git a/docs/settings/logging-settings.asciidoc b/docs/settings/logging-settings.asciidoc index 77f3bd90a911..177d1bc8db11 100644 --- a/docs/settings/logging-settings.asciidoc +++ b/docs/settings/logging-settings.asciidoc @@ -12,16 +12,6 @@ Refer to the <> for common configuration use cases. To learn more about possible configuration values, go to {kibana-ref}/logging-service.html[{kib}'s Logging service]. -[[log-settings-compatibility]] -==== Backwards compatibility -Compatibility with the legacy logging system is assured until the end of the `v7` version. -All log messages handled by `root` context (default) are forwarded to the legacy logging service. -The logging configuration is validated against the predefined schema and if there are -any issues with it, {kib} will fail to start with the detailed error message. - -NOTE: When you switch to the new logging configuration, you will start seeing duplicate log entries in both formats. -These will be removed when the `default` appender is no longer required. - [[log-settings-examples]] ==== Examples Here are some configuration examples for the most common logging use cases: diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 3f12925bbef0..3b9868178fa8 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[alerting-setup]] -== Alerting Set up +== Alerting set up ++++ Set up ++++ @@ -20,6 +20,8 @@ If you are using an *on-premises* Elastic Stack deployment with <>. {kib} alerting uses <> to secure background rule checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. * If you have enabled TLS and are still unable to access Alerting, ensure that you have not {ref}/security-settings.html#api-key-service-settings[explicitly disabled API keys]. +The Alerting framework uses queries that require the `search.allow_expensive_queries` setting to be `true`. See the scripts {ref}/query-dsl-script-query.html#_allow_expensive_queries_4[documentation]. + [float] [[alerting-setup-production]] === Production considerations and scaling guidance diff --git a/docs/user/production-considerations/production.asciidoc b/docs/user/production-considerations/production.asciidoc index 455e07e45280..db8d0738323f 100644 --- a/docs/user/production-considerations/production.asciidoc +++ b/docs/user/production-considerations/production.asciidoc @@ -32,12 +32,21 @@ server.name Settings unique across each host (for example, running multiple installations on the same virtual machine): [source,js] -------- -logging.dest path.data pid.file server.port -------- +When using a file appender, the target file must also be unique: +[source,yaml] +-------- +logging: + appenders: + default: + type: file + fileName: /unique/path/per/instance +-------- + Settings that must be the same: [source,js] -------- diff --git a/examples/expressions_explorer/public/actions_and_expressions.tsx b/examples/expressions_explorer/public/actions_and_expressions.tsx index f802a78faf06..6d0c8886a79f 100644 --- a/examples/expressions_explorer/public/actions_and_expressions.tsx +++ b/examples/expressions_explorer/public/actions_and_expressions.tsx @@ -47,11 +47,11 @@ export function ActionsExpressionsExample({ expressions, actions }: Props) { }; const handleEvents = (event: any) => { - if (event.id !== 'NAVIGATE') return; + if (event.name !== 'NAVIGATE') return; // enrich event context with some extra data event.baseUrl = 'http://www.google.com'; - actions.executeTriggerActions(NAVIGATE_TRIGGER_ID, event.value); + actions.executeTriggerActions(NAVIGATE_TRIGGER_ID, event.data); }; return ( diff --git a/examples/expressions_explorer/public/actions_and_expressions2.tsx b/examples/expressions_explorer/public/actions_and_expressions2.tsx index 31ba903ad91a..e7dc28b8b97c 100644 --- a/examples/expressions_explorer/public/actions_and_expressions2.tsx +++ b/examples/expressions_explorer/public/actions_and_expressions2.tsx @@ -50,7 +50,7 @@ export function ActionsExpressionsExample2({ expressions, actions }: Props) { }; const handleEvents = (event: any) => { - updateVariables({ color: event.value.href === 'http://www.google.com' ? 'red' : 'blue' }); + updateVariables({ color: event.data.href === 'http://www.google.com' ? 'red' : 'blue' }); }; return ( diff --git a/examples/expressions_explorer/public/renderers/button.tsx b/examples/expressions_explorer/public/renderers/button.tsx index 68add91c3cbc..557180ab73a3 100644 --- a/examples/expressions_explorer/public/renderers/button.tsx +++ b/examples/expressions_explorer/public/renderers/button.tsx @@ -18,8 +18,8 @@ export const buttonRenderer: ExpressionRenderDefinition = { render(domNode, config, handlers) { const buttonClick = () => { handlers.event({ - id: 'NAVIGATE', - value: { + name: 'NAVIGATE', + data: { href: config.href, }, }); diff --git a/legacy_rfcs/README.md b/legacy_rfcs/README.md index f9f9502ad954..4ef4db56c004 100644 --- a/legacy_rfcs/README.md +++ b/legacy_rfcs/README.md @@ -1,3 +1,3 @@ # Kibana RFCs -We no longer follow this RFC process. Internal developers should review the new RFC process in our [internal developer guide](https://docs.elastic.dev/kibana-team/rfc-process) +We no longer follow this RFC process. Internal developers should review the new RFC process in our [internal developer guide](https://docs.elastic.dev/kibana-dev-docs/contributing/rfc-process) diff --git a/package.json b/package.json index ac30b5de6f48..37a5239cf068 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,6 @@ "@elastic/ems-client": "7.15.0", "@elastic/eui": "38.0.1", "@elastic/filesaver": "1.1.2", - "@elastic/good": "^9.0.1-kibana3", "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.1", @@ -113,12 +112,10 @@ "@hapi/accept": "^5.0.2", "@hapi/boom": "^9.1.4", "@hapi/cookie": "^11.0.2", - "@hapi/good-squeeze": "6.0.0", "@hapi/h2o2": "^9.1.0", "@hapi/hapi": "^20.2.0", "@hapi/hoek": "^9.2.0", "@hapi/inert": "^6.0.4", - "@hapi/podium": "^4.1.3", "@hapi/wreck": "^17.1.0", "@kbn/ace": "link:bazel-bin/packages/kbn-ace", "@kbn/alerts": "link:bazel-bin/packages/kbn-alerts", @@ -133,7 +130,6 @@ "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n", "@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter", "@kbn/io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils", - "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging", "@kbn/logging": "link:bazel-bin/packages/kbn-logging", "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl", "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco", @@ -272,7 +268,6 @@ "json-stable-stringify": "^1.0.1", "json-stringify-pretty-compact": "1.2.0", "json-stringify-safe": "5.0.1", - "jsonwebtoken": "^8.5.1", "jsts": "^1.6.2", "kea": "^2.4.2", "load-json-file": "^6.2.0", @@ -554,7 +549,6 @@ "@types/jsdom": "^16.2.3", "@types/json-stable-stringify": "^1.0.32", "@types/json5": "^0.0.30", - "@types/jsonwebtoken": "^8.5.5", "@types/license-checker": "15.0.0", "@types/listr": "^0.14.0", "@types/loader-utils": "^1.1.3", @@ -662,7 +656,6 @@ "babel-plugin-styled-components": "^1.13.2", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "backport": "^5.6.6", - "base64url": "^3.0.1", "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", @@ -770,7 +763,6 @@ "multimatch": "^4.0.0", "mutation-observer": "^1.0.3", "ncp": "^2.0.0", - "node-jq": "2.0.0", "node-sass": "^6.0.1", "null-loader": "^3.0.0", "nyc": "^15.0.1", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 36bdee5303cb..75c8d700e284 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -29,7 +29,6 @@ filegroup( "//packages/kbn-i18n:build", "//packages/kbn-interpreter:build", "//packages/kbn-io-ts-utils:build", - "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", "//packages/kbn-mapbox-gl:build", "//packages/kbn-monaco:build", diff --git a/packages/kbn-cli-dev-mode/src/bootstrap.ts b/packages/kbn-cli-dev-mode/src/bootstrap.ts index 86a276c64f1f..0428051b77e3 100644 --- a/packages/kbn-cli-dev-mode/src/bootstrap.ts +++ b/packages/kbn-cli-dev-mode/src/bootstrap.ts @@ -20,7 +20,7 @@ interface BootstrapArgs { } export async function bootstrapDevMode({ configs, cliArgs, applyConfigOverrides }: BootstrapArgs) { - const log = new CliLog(!!cliArgs.quiet, !!cliArgs.silent); + const log = new CliLog(!!cliArgs.silent); const env = Env.createDefault(REPO_ROOT, { configs, diff --git a/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts index 8937eadfa4ee..e5e009e51e69 100644 --- a/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts +++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts @@ -74,7 +74,6 @@ const createCliArgs = (parts: Partial = {}): SomeCliArgs => ({ runExamples: false, watch: true, silent: false, - quiet: false, ...parts, }); diff --git a/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts index 28f38592ff3c..2396b316aa3a 100644 --- a/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts +++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts @@ -48,7 +48,6 @@ const GRACEFUL_TIMEOUT = 30000; export type SomeCliArgs = Pick< CliArgs, - | 'quiet' | 'silent' | 'verbose' | 'disableOptimizer' @@ -108,7 +107,7 @@ export class CliDevMode { private subscription?: Rx.Subscription; constructor({ cliArgs, config, log }: { cliArgs: SomeCliArgs; config: CliDevConfig; log?: Log }) { - this.log = log || new CliLog(!!cliArgs.quiet, !!cliArgs.silent); + this.log = log || new CliLog(!!cliArgs.silent); if (cliArgs.basePath) { this.basePathProxy = new BasePathProxyServer(this.log, config.http, config.dev); @@ -163,7 +162,7 @@ export class CliDevMode { runExamples: cliArgs.runExamples, cache: cliArgs.cache, dist: cliArgs.dist, - quiet: !!cliArgs.quiet, + quiet: false, silent: !!cliArgs.silent, verbose: !!cliArgs.verbose, watch: cliArgs.watch, diff --git a/packages/kbn-cli-dev-mode/src/dev_server.test.ts b/packages/kbn-cli-dev-mode/src/dev_server.test.ts index 9962a9a285a4..92dbe484eb00 100644 --- a/packages/kbn-cli-dev-mode/src/dev_server.test.ts +++ b/packages/kbn-cli-dev-mode/src/dev_server.test.ts @@ -130,7 +130,6 @@ describe('#run$', () => { Array [ "foo", "bar", - "--logging.json=false", ], Object { "env": Object { diff --git a/packages/kbn-cli-dev-mode/src/log.ts b/packages/kbn-cli-dev-mode/src/log.ts index 86956abec202..2cbd02b94a84 100644 --- a/packages/kbn-cli-dev-mode/src/log.ts +++ b/packages/kbn-cli-dev-mode/src/log.ts @@ -21,7 +21,7 @@ export interface Log { export class CliLog implements Log { public toolingLog = new ToolingLog({ - level: this.silent ? 'silent' : this.quiet ? 'error' : 'info', + level: this.silent ? 'silent' : 'info', writeTo: { write: (msg) => { this.write(msg); @@ -29,10 +29,10 @@ export class CliLog implements Log { }, }); - constructor(private readonly quiet: boolean, private readonly silent: boolean) {} + constructor(private readonly silent: boolean) {} good(label: string, ...args: any[]) { - if (this.quiet || this.silent) { + if (this.silent) { return; } @@ -41,7 +41,7 @@ export class CliLog implements Log { } warn(label: string, ...args: any[]) { - if (this.quiet || this.silent) { + if (this.silent) { return; } diff --git a/packages/kbn-cli-dev-mode/src/using_server_process.ts b/packages/kbn-cli-dev-mode/src/using_server_process.ts index 0d0227c63adc..eb997295035d 100644 --- a/packages/kbn-cli-dev-mode/src/using_server_process.ts +++ b/packages/kbn-cli-dev-mode/src/using_server_process.ts @@ -25,7 +25,7 @@ export function usingServerProcess( ) { return Rx.using( (): ProcResource => { - const proc = execa.node(script, [...argv, '--logging.json=false'], { + const proc = execa.node(script, argv, { stdio: 'pipe', nodeOptions: [ ...process.execArgv, diff --git a/packages/kbn-config/src/__mocks__/env.ts b/packages/kbn-config/src/__mocks__/env.ts index 6f05f8f1f5a4..124a798501a9 100644 --- a/packages/kbn-config/src/__mocks__/env.ts +++ b/packages/kbn-config/src/__mocks__/env.ts @@ -19,7 +19,6 @@ export function getEnvOptions(options: DeepPartial = {}): EnvOptions configs: options.configs || [], cliArgs: { dev: true, - quiet: false, silent: false, watch: false, basePath: false, diff --git a/packages/kbn-config/src/__snapshots__/env.test.ts.snap b/packages/kbn-config/src/__snapshots__/env.test.ts.snap index 570ed948774c..a8e2eb62dbed 100644 --- a/packages/kbn-config/src/__snapshots__/env.test.ts.snap +++ b/packages/kbn-config/src/__snapshots__/env.test.ts.snap @@ -11,7 +11,6 @@ Env { "dist": false, "envName": "development", "oss": false, - "quiet": false, "runExamples": false, "silent": false, "watch": false, @@ -54,7 +53,6 @@ Env { "dist": false, "envName": "production", "oss": false, - "quiet": false, "runExamples": false, "silent": false, "watch": false, @@ -96,7 +94,6 @@ Env { "disableOptimizer": true, "dist": false, "oss": false, - "quiet": false, "runExamples": false, "silent": false, "watch": false, @@ -138,7 +135,6 @@ Env { "disableOptimizer": true, "dist": false, "oss": false, - "quiet": false, "runExamples": false, "silent": false, "watch": false, @@ -180,7 +176,6 @@ Env { "disableOptimizer": true, "dist": false, "oss": false, - "quiet": false, "runExamples": false, "silent": false, "watch": false, @@ -222,7 +217,6 @@ Env { "disableOptimizer": true, "dist": false, "oss": false, - "quiet": false, "runExamples": false, "silent": false, "watch": false, diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts index 5883ce8ab513..72725fad3961 100644 --- a/packages/kbn-config/src/config_service.ts +++ b/packages/kbn-config/src/config_service.ts @@ -24,7 +24,7 @@ import { DeprecatedConfigDetails, ChangedDeprecatedPaths, } from './deprecation'; -import { LegacyObjectToConfigAdapter } from './legacy'; +import { ObjectToConfigAdapter } from './object_to_config_adapter'; /** @internal */ export type IConfigService = PublicMethodsOf; @@ -71,7 +71,7 @@ export class ConfigService { map(([rawConfig, deprecations]) => { const migrated = applyDeprecations(rawConfig, deprecations); this.deprecatedConfigPaths.next(migrated.changedPaths); - return new LegacyObjectToConfigAdapter(migrated.config); + return new ObjectToConfigAdapter(migrated.config); }), tap((config) => { this.lastConfig = config; diff --git a/packages/kbn-config/src/env.ts b/packages/kbn-config/src/env.ts index 053bb93ce158..73f32606c463 100644 --- a/packages/kbn-config/src/env.ts +++ b/packages/kbn-config/src/env.ts @@ -21,8 +21,6 @@ export interface EnvOptions { export interface CliArgs { dev: boolean; envName?: string; - /** @deprecated */ - quiet?: boolean; silent?: boolean; verbose?: boolean; watch: boolean; diff --git a/packages/kbn-config/src/index.ts b/packages/kbn-config/src/index.ts index 08cf12343f45..89f70ab9b698 100644 --- a/packages/kbn-config/src/index.ts +++ b/packages/kbn-config/src/index.ts @@ -30,5 +30,4 @@ export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './c export { ObjectToConfigAdapter } from './object_to_config_adapter'; export { CliArgs, Env, RawPackageInfo } from './env'; export { EnvironmentMode, PackageInfo } from './types'; -export { LegacyObjectToConfigAdapter, LegacyLoggingConfig } from './legacy'; export { getPluginSearchPaths } from './plugins'; diff --git a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap deleted file mode 100644 index 17ac75e9f3d9..000000000000 --- a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ /dev/null @@ -1,95 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#get correctly handles silent logging config. 1`] = ` -Object { - "appenders": Object { - "default": Object { - "legacyLoggingConfig": Object { - "silent": true, - }, - "type": "legacy-appender", - }, - }, - "loggers": undefined, - "root": Object { - "level": "off", - }, - "silent": true, -} -`; - -exports[`#get correctly handles verbose file logging config with json format. 1`] = ` -Object { - "appenders": Object { - "default": Object { - "legacyLoggingConfig": Object { - "dest": "/some/path.log", - "json": true, - "verbose": true, - }, - "type": "legacy-appender", - }, - }, - "dest": "/some/path.log", - "json": true, - "loggers": undefined, - "root": Object { - "level": "all", - }, - "verbose": true, -} -`; - -exports[`#getFlattenedPaths returns all paths of the underlying object. 1`] = ` -Array [ - "known", - "knownContainer.sub1", - "knownContainer.sub2", - "legacy.known", -] -`; - -exports[`#set correctly sets values for existing paths. 1`] = ` -Object { - "known": "value", - "knownContainer": Object { - "sub1": "sub-value-1", - "sub2": "sub-value-2", - }, -} -`; - -exports[`#set correctly sets values for paths that do not exist. 1`] = ` -Object { - "unknown": Object { - "sub1": "sub-value-1", - "sub2": "sub-value-2", - }, -} -`; - -exports[`#toRaw returns a deep copy of the underlying raw config object. 1`] = ` -Object { - "known": "foo", - "knownContainer": Object { - "sub1": "bar", - "sub2": "baz", - }, - "legacy": Object { - "known": "baz", - }, -} -`; - -exports[`#toRaw returns a deep copy of the underlying raw config object. 2`] = ` -Object { - "known": "bar", - "knownContainer": Object { - "sub1": "baz", - "sub2": "baz", - }, - "legacy": Object { - "known": "baz", - }, -} -`; diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts deleted file mode 100644 index 47151503e163..000000000000 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { LegacyObjectToConfigAdapter } from './legacy_object_to_config_adapter'; - -describe('#get', () => { - test('correctly handles paths that do not exist.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({}); - - expect(configAdapter.get('one')).not.toBeDefined(); - expect(configAdapter.get(['one', 'two'])).not.toBeDefined(); - expect(configAdapter.get(['one.three'])).not.toBeDefined(); - }); - - test('correctly handles paths that do not need to be transformed.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({ - one: 'value-one', - two: { - sub: 'value-two-sub', - }, - container: { - value: 'some', - }, - }); - - expect(configAdapter.get('one')).toEqual('value-one'); - expect(configAdapter.get(['two', 'sub'])).toEqual('value-two-sub'); - expect(configAdapter.get('two.sub')).toEqual('value-two-sub'); - expect(configAdapter.get('container')).toEqual({ value: 'some' }); - }); - - test('correctly handles csp config.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({ - csp: { - rules: ['strict'], - }, - }); - - expect(configAdapter.get('csp')).toMatchInlineSnapshot(` - Object { - "rules": Array [ - "strict", - ], - } - `); - }); - - test('correctly handles silent logging config.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({ - logging: { silent: true }, - }); - - expect(configAdapter.get('logging')).toMatchSnapshot(); - }); - - test('correctly handles verbose file logging config with json format.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({ - logging: { verbose: true, json: true, dest: '/some/path.log' }, - }); - - expect(configAdapter.get('logging')).toMatchSnapshot(); - }); -}); - -describe('#set', () => { - test('correctly sets values for paths that do not exist.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({}); - - configAdapter.set('unknown', 'value'); - configAdapter.set(['unknown', 'sub1'], 'sub-value-1'); - configAdapter.set('unknown.sub2', 'sub-value-2'); - - expect(configAdapter.toRaw()).toMatchSnapshot(); - }); - - test('correctly sets values for existing paths.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({ - known: '', - knownContainer: { - sub1: 'sub-1', - sub2: 'sub-2', - }, - }); - - configAdapter.set('known', 'value'); - configAdapter.set(['knownContainer', 'sub1'], 'sub-value-1'); - configAdapter.set('knownContainer.sub2', 'sub-value-2'); - - expect(configAdapter.toRaw()).toMatchSnapshot(); - }); -}); - -describe('#has', () => { - test('returns false if config is not set', () => { - const configAdapter = new LegacyObjectToConfigAdapter({}); - - expect(configAdapter.has('unknown')).toBe(false); - expect(configAdapter.has(['unknown', 'sub1'])).toBe(false); - expect(configAdapter.has('unknown.sub2')).toBe(false); - }); - - test('returns true if config is set.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({ - known: 'foo', - knownContainer: { - sub1: 'bar', - sub2: 'baz', - }, - }); - - expect(configAdapter.has('known')).toBe(true); - expect(configAdapter.has(['knownContainer', 'sub1'])).toBe(true); - expect(configAdapter.has('knownContainer.sub2')).toBe(true); - }); -}); - -describe('#toRaw', () => { - test('returns a deep copy of the underlying raw config object.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({ - known: 'foo', - knownContainer: { - sub1: 'bar', - sub2: 'baz', - }, - legacy: { known: 'baz' }, - }); - - const firstRawCopy = configAdapter.toRaw(); - - configAdapter.set('known', 'bar'); - configAdapter.set(['knownContainer', 'sub1'], 'baz'); - - const secondRawCopy = configAdapter.toRaw(); - - expect(firstRawCopy).not.toBe(secondRawCopy); - expect(firstRawCopy.knownContainer).not.toBe(secondRawCopy.knownContainer); - - expect(firstRawCopy).toMatchSnapshot(); - expect(secondRawCopy).toMatchSnapshot(); - }); -}); - -describe('#getFlattenedPaths', () => { - test('returns all paths of the underlying object.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({ - known: 'foo', - knownContainer: { - sub1: 'bar', - sub2: 'baz', - }, - legacy: { known: 'baz' }, - }); - - expect(configAdapter.getFlattenedPaths()).toMatchSnapshot(); - }); -}); diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts deleted file mode 100644 index bc6fd49e2498..000000000000 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ConfigPath } from '../config'; -import { ObjectToConfigAdapter } from '../object_to_config_adapter'; - -/** - * Represents logging config supported by the legacy platform. - */ -export interface LegacyLoggingConfig { - silent?: boolean; - verbose?: boolean; - quiet?: boolean; - dest?: string; - json?: boolean; - events?: Record; -} - -type MixedLoggingConfig = LegacyLoggingConfig & Record; - -/** - * Represents adapter between config provided by legacy platform and `Config` - * supported by the current platform. - * @internal - */ -export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { - private static transformLogging(configValue: MixedLoggingConfig = {}) { - const { appenders, root, loggers, ...legacyLoggingConfig } = configValue; - - const loggingConfig = { - appenders: { - ...appenders, - default: { type: 'legacy-appender', legacyLoggingConfig }, - }, - root: { level: 'info', ...root }, - loggers, - ...legacyLoggingConfig, - }; - - if (configValue.silent) { - loggingConfig.root.level = 'off'; - } else if (configValue.quiet) { - loggingConfig.root.level = 'error'; - } else if (configValue.verbose) { - loggingConfig.root.level = 'all'; - } - - return loggingConfig; - } - - public get(configPath: ConfigPath) { - const configValue = super.get(configPath); - switch (configPath) { - case 'logging': - return LegacyObjectToConfigAdapter.transformLogging(configValue as LegacyLoggingConfig); - default: - return configValue; - } - } -} diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index 4d6ea646b2ab..45d31c1eefad 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -82,6 +82,7 @@ export class CiStatsReporter { const upstreamBranch = options.upstreamBranch ?? this.getUpstreamBranch(); const kibanaUuid = options.kibanaUuid === undefined ? this.getKibanaUuid() : options.kibanaUuid; let email; + let branch; try { const { stdout } = await execa('git', ['config', 'user.email']); @@ -90,19 +91,33 @@ export class CiStatsReporter { this.log.debug(e.message); } + try { + const { stdout } = await execa('git', ['branch', '--show-current']); + branch = stdout; + } catch (e) { + this.log.debug(e.message); + } + + const memUsage = process.memoryUsage(); const isElasticCommitter = email && email.endsWith('@elastic.co') ? true : false; const defaultMetadata = { + kibanaUuid, + isElasticCommitter, committerHash: email ? crypto.createHash('sha256').update(email).digest('hex').substring(0, 20) : undefined, + email: isElasticCommitter ? email : undefined, + branch: isElasticCommitter ? branch : undefined, cpuCount: Os.cpus()?.length, cpuModel: Os.cpus()[0]?.model, cpuSpeed: Os.cpus()[0]?.speed, - email: isElasticCommitter ? email : undefined, freeMem: Os.freemem(), - isElasticCommitter, - kibanaUuid, + memoryUsageRss: memUsage.rss, + memoryUsageHeapTotal: memUsage.heapTotal, + memoryUsageHeapUsed: memUsage.heapUsed, + memoryUsageExternal: memUsage.external, + memoryUsageArrayBuffers: memUsage.arrayBuffers, nestedTiming: process.env.CI_STATS_NESTED_TIMING ? true : false, osArch: Os.arch(), osPlatform: Os.platform(), diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts index d99217c38b41..9cb05608526e 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts @@ -8,3 +8,4 @@ export * from './ci_stats_reporter'; export * from './ship_ci_stats_cli'; +export { getTimeReporter } from './report_time'; diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/report_time.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/report_time.ts new file mode 100644 index 000000000000..d10250a03f09 --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/report_time.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CiStatsReporter, ToolingLog } from '..'; + +export const getTimeReporter = (log: ToolingLog, group: string) => { + const reporter = CiStatsReporter.fromEnv(log); + return async (startTime: number, id: string, meta: Record) => { + await reporter.timings({ + timings: [ + { + group, + id, + ms: Date.now() - startTime, + meta, + }, + ], + }); + }; +}; diff --git a/packages/kbn-es/src/cli_commands/snapshot.js b/packages/kbn-es/src/cli_commands/snapshot.js index 7f5653db72b4..e64dcb7c7731 100644 --- a/packages/kbn-es/src/cli_commands/snapshot.js +++ b/packages/kbn-es/src/cli_commands/snapshot.js @@ -8,6 +8,7 @@ const dedent = require('dedent'); const getopts = require('getopts'); +import { ToolingLog, getTimeReporter } from '@kbn/dev-utils'; const { Cluster } = require('../cluster'); exports.description = 'Downloads and run from a nightly snapshot'; @@ -36,6 +37,13 @@ exports.help = (defaults = {}) => { }; exports.run = async (defaults = {}) => { + const runStartTime = Date.now(); + const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, + }); + const reportTime = getTimeReporter(log, 'scripts/es snapshot'); + const argv = process.argv.slice(2); const options = getopts(argv, { alias: { @@ -56,12 +64,22 @@ exports.run = async (defaults = {}) => { if (options['download-only']) { await cluster.downloadSnapshot(options); } else { + const installStartTime = Date.now(); const { installPath } = await cluster.installSnapshot(options); if (options.dataArchive) { await cluster.extractDataDirectory(installPath, options.dataArchive); } - await cluster.run(installPath, options); + reportTime(installStartTime, 'installed', { + success: true, + ...options, + }); + + await cluster.run(installPath, { + reportTime, + startTime: runStartTime, + ...options, + }); } }; diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index ac4380da88be..0866b14f4ade 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -240,7 +240,7 @@ exports.Cluster = class Cluster { * @return {undefined} */ _exec(installPath, opts = {}) { - const { skipNativeRealmSetup = false, ...options } = opts; + const { skipNativeRealmSetup = false, reportTime = () => {}, startTime, ...options } = opts; if (this._process || this._outcome) { throw new Error('ES has already been started'); @@ -321,10 +321,17 @@ exports.Cluster = class Cluster { await nativeRealm.setPasswords(options); }); + let reportSent = false; // parse and forward es stdout to the log this._process.stdout.on('data', (data) => { const lines = parseEsLog(data.toString()); lines.forEach((line) => { + if (!reportSent && line.message.includes('publish_address')) { + reportSent = true; + reportTime(startTime, 'ready', { + success: true, + }); + } this._log.info(line.formattedMessage); }); }); @@ -341,7 +348,16 @@ exports.Cluster = class Cluster { // JVM exits with 143 on SIGTERM and 130 on SIGINT, dont' treat them as errors if (code > 0 && !(code === 143 || code === 130)) { + reportTime(startTime, 'abort', { + success: true, + error: code, + }); throw createCliError(`ES exited with code ${code}`); + } else { + reportTime(startTime, 'error', { + success: false, + error: `exited with ${code}`, + }); } }); } diff --git a/packages/kbn-legacy-logging/BUILD.bazel b/packages/kbn-legacy-logging/BUILD.bazel deleted file mode 100644 index c4927fe076e1..000000000000 --- a/packages/kbn-legacy-logging/BUILD.bazel +++ /dev/null @@ -1,107 +0,0 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") - -PKG_BASE_NAME = "kbn-legacy-logging" -PKG_REQUIRE_NAME = "@kbn/legacy-logging" - -SOURCE_FILES = glob( - [ - "src/**/*.ts", - ], - exclude = ["**/*.test.*"], -) - -SRCS = SOURCE_FILES - -filegroup( - name = "srcs", - srcs = SRCS, -) - -NPM_MODULE_EXTRA_FILES = [ - "package.json", - "README.md" -] - -RUNTIME_DEPS = [ - "//packages/kbn-config-schema", - "//packages/kbn-utils", - "@npm//@elastic/numeral", - "@npm//@hapi/hapi", - "@npm//@hapi/podium", - "@npm//chokidar", - "@npm//lodash", - "@npm//moment-timezone", - "@npm//query-string", - "@npm//rxjs", - "@npm//tslib", -] - -TYPES_DEPS = [ - "//packages/kbn-config-schema", - "//packages/kbn-utils", - "@npm//@elastic/numeral", - "@npm//@hapi/podium", - "@npm//chokidar", - "@npm//query-string", - "@npm//rxjs", - "@npm//tslib", - "@npm//@types/hapi__hapi", - "@npm//@types/jest", - "@npm//@types/lodash", - "@npm//@types/moment-timezone", - "@npm//@types/node", -] - -jsts_transpiler( - name = "target_node", - srcs = SRCS, - build_pkg_name = package_name(), -) - -ts_config( - name = "tsconfig", - src = "tsconfig.json", - deps = [ - "//:tsconfig.base.json", - "//:tsconfig.bazel.json", - ], -) - -ts_project( - name = "tsc_types", - args = ['--pretty'], - srcs = SRCS, - deps = TYPES_DEPS, - declaration = True, - declaration_map = True, - emit_declaration_only = True, - out_dir = "target_types", - source_map = True, - root_dir = "src", - tsconfig = ":tsconfig", -) - -js_library( - name = PKG_BASE_NAME, - srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], - package_name = PKG_REQUIRE_NAME, - visibility = ["//visibility:public"], -) - -pkg_npm( - name = "npm_module", - deps = [ - ":%s" % PKG_BASE_NAME, - ] -) - -filegroup( - name = "build", - srcs = [ - ":npm_module", - ], - visibility = ["//visibility:public"], -) diff --git a/packages/kbn-legacy-logging/README.md b/packages/kbn-legacy-logging/README.md deleted file mode 100644 index 4c5989fc892d..000000000000 --- a/packages/kbn-legacy-logging/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# @kbn/legacy-logging - -This package contains the implementation of the legacy logging -system, based on `@hapi/good` \ No newline at end of file diff --git a/packages/kbn-legacy-logging/package.json b/packages/kbn-legacy-logging/package.json deleted file mode 100644 index 6e846ffc5bfa..000000000000 --- a/packages/kbn-legacy-logging/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@kbn/legacy-logging", - "version": "1.0.0", - "private": true, - "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target_node/index.js", - "types": "./target_types/index.d.ts" -} diff --git a/packages/kbn-legacy-logging/src/get_logging_config.ts b/packages/kbn-legacy-logging/src/get_logging_config.ts deleted file mode 100644 index f74bc5904e24..000000000000 --- a/packages/kbn-legacy-logging/src/get_logging_config.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import { getLogReporter } from './log_reporter'; -import { LegacyLoggingConfig } from './schema'; - -/** - * Returns the `@hapi/good` plugin configuration to be used for the legacy logging - * @param config - */ -export function getLoggingConfiguration(config: LegacyLoggingConfig, opsInterval: number) { - const events = config.events; - - if (config.silent) { - _.defaults(events, {}); - } else if (config.quiet) { - _.defaults(events, { - log: ['listening', 'error', 'fatal'], - request: ['error'], - error: '*', - }); - } else if (config.verbose) { - _.defaults(events, { - error: '*', - log: '*', - // To avoid duplicate logs, we explicitly disable these in verbose - // mode as they are already provided by the new logging config under - // the `http.server.response` and `metrics.ops` contexts. - ops: '!', - request: '!', - response: '!', - }); - } else { - _.defaults(events, { - log: ['info', 'warning', 'error', 'fatal'], - request: ['info', 'warning', 'error', 'fatal'], - error: '*', - }); - } - - const loggerStream = getLogReporter({ - config: { - json: config.json, - dest: config.dest, - timezone: config.timezone, - - // I'm adding the default here because if you add another filter - // using the commandline it will remove authorization. I want users - // to have to explicitly set --logging.filter.authorization=none or - // --logging.filter.cookie=none to have it show up in the logs. - filter: _.defaults(config.filter, { - authorization: 'remove', - cookie: 'remove', - }), - }, - events: _.transform( - events, - function (filtered: Record, val: string, key: string) { - // provide a string compatible way to remove events - if (val !== '!') filtered[key] = val; - }, - {} - ), - }); - - const options = { - ops: { - interval: opsInterval, - }, - includes: { - request: ['headers', 'payload'], - response: ['headers', 'payload'], - }, - reporters: { - logReporter: [loggerStream], - }, - }; - return options; -} diff --git a/packages/kbn-legacy-logging/src/index.ts b/packages/kbn-legacy-logging/src/index.ts deleted file mode 100644 index 670df4e95f33..000000000000 --- a/packages/kbn-legacy-logging/src/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { LegacyLoggingConfig, legacyLoggingConfigSchema } from './schema'; -export { attachMetaData } from './metadata'; -export { setupLoggingRotate } from './rotate'; -export { setupLogging, reconfigureLogging } from './setup_logging'; -export { getLoggingConfiguration } from './get_logging_config'; -export { LegacyLoggingServer } from './legacy_logging_server'; diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.test.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.test.ts deleted file mode 100644 index 40019fc90ff4..000000000000 --- a/packages/kbn-legacy-logging/src/legacy_logging_server.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -jest.mock('./setup_logging'); - -import { LegacyLoggingServer, LogRecord } from './legacy_logging_server'; - -test('correctly forwards log records.', () => { - const loggingServer = new LegacyLoggingServer({ events: {} }); - const onLogMock = jest.fn(); - loggingServer.events.on('log', onLogMock); - - const timestamp = 1554433221100; - const firstLogRecord: LogRecord = { - timestamp: new Date(timestamp), - pid: 5355, - level: { - id: 'info', - value: 5, - }, - context: 'some-context', - message: 'some-message', - }; - - const secondLogRecord: LogRecord = { - timestamp: new Date(timestamp), - pid: 5355, - level: { - id: 'error', - value: 3, - }, - context: 'some-context.sub-context', - message: 'some-message', - meta: { unknown: 2 }, - error: new Error('some-error'), - }; - - const thirdLogRecord: LogRecord = { - timestamp: new Date(timestamp), - pid: 5355, - level: { - id: 'trace', - value: 7, - }, - context: 'some-context.sub-context', - message: 'some-message', - meta: { tags: ['important', 'tags'], unknown: 2 }, - }; - - loggingServer.log(firstLogRecord); - loggingServer.log(secondLogRecord); - loggingServer.log(thirdLogRecord); - - expect(onLogMock).toHaveBeenCalledTimes(3); - - const [[firstCall], [secondCall], [thirdCall]] = onLogMock.mock.calls; - expect(firstCall).toMatchInlineSnapshot(` -Object { - "data": "some-message", - "tags": Array [ - "info", - "some-context", - ], - "timestamp": 1554433221100, -} -`); - - expect(secondCall).toMatchInlineSnapshot(` -Object { - "data": [Error: some-error], - "tags": Array [ - "error", - "some-context", - "sub-context", - ], - "timestamp": 1554433221100, -} -`); - - expect(thirdCall).toMatchInlineSnapshot(` -Object { - "data": Object { - Symbol(log message with metadata): Object { - "message": "some-message", - "metadata": Object { - "unknown": 2, - }, - }, - }, - "tags": Array [ - "debug", - "some-context", - "sub-context", - "important", - "tags", - ], - "timestamp": 1554433221100, -} -`); -}); diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.ts deleted file mode 100644 index f6c42dd1b161..000000000000 --- a/packages/kbn-legacy-logging/src/legacy_logging_server.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ServerExtType, Server } from '@hapi/hapi'; -import Podium from '@hapi/podium'; -import { setupLogging } from './setup_logging'; -import { attachMetaData } from './metadata'; -import { legacyLoggingConfigSchema } from './schema'; - -// these LogXXX types are duplicated to avoid a cross dependency with the @kbn/logging package. -// typescript will error if they diverge at some point. -type LogLevelId = 'all' | 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'off'; - -interface LogLevel { - id: LogLevelId; - value: number; -} - -export interface LogRecord { - timestamp: Date; - level: LogLevel; - context: string; - message: string; - error?: Error; - meta?: { [name: string]: any }; - pid: number; -} - -const isEmptyObject = (obj: object) => Object.keys(obj).length === 0; - -function getDataToLog(error: Error | undefined, metadata: object, message: string) { - if (error) { - return error; - } - if (!isEmptyObject(metadata)) { - return attachMetaData(message, metadata); - } - return message; -} - -interface PluginRegisterParams { - plugin: { - register: ( - server: LegacyLoggingServer, - options: PluginRegisterParams['options'] - ) => Promise; - }; - options: Record; -} - -/** - * Converts core log level to a one that's known to the legacy platform. - * @param level Log level from the core. - */ -function getLegacyLogLevel(level: LogLevel) { - const logLevel = level.id.toLowerCase(); - if (logLevel === 'warn') { - return 'warning'; - } - - if (logLevel === 'trace') { - return 'debug'; - } - - return logLevel; -} - -/** - * The "legacy" Kibana uses Hapi server + even-better plugin to log, so we should - * use the same approach here to make log records generated by the core to look the - * same as the rest of the records generated by the "legacy" Kibana. But to reduce - * overhead of having full blown Hapi server instance we create our own "light" version. - * @internal - */ -export class LegacyLoggingServer { - public connections = []; - // Emulates Hapi's usage of the podium event bus. - public events: Podium = new Podium(['log', 'request', 'response']); - - private onPostStopCallback?: () => void; - - constructor(legacyLoggingConfig: any) { - // We set `ops.interval` to max allowed number and `ops` filter to value - // that doesn't exist to avoid logging of ops at all, if turned on it will be - // logged by the "legacy" Kibana. - const loggingConfig = legacyLoggingConfigSchema.validate({ - ...legacyLoggingConfig, - events: { - ...legacyLoggingConfig.events, - ops: '__no-ops__', - }, - }); - - setupLogging(this as unknown as Server, loggingConfig, 2147483647); - } - - public register({ plugin: { register }, options }: PluginRegisterParams): Promise { - return register(this, options); - } - - public log({ level, context, message, error, timestamp, meta = {} }: LogRecord) { - const { tags = [], ...metadata } = meta; - - this.events - .emit('log', { - data: getDataToLog(error, metadata, message), - tags: [getLegacyLogLevel(level), ...context.split('.'), ...tags], - timestamp: timestamp.getTime(), - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.error('An unexpected error occurred while writing to the log:', err.stack); - process.exit(1); - }); - } - - public stop() { - // Tell the plugin we're stopping. - if (this.onPostStopCallback !== undefined) { - this.onPostStopCallback(); - } - } - - public ext(eventName: ServerExtType, callback: () => void) { - // method is called by plugin that's being registered. - if (eventName === 'onPostStop') { - this.onPostStopCallback = callback; - } - // We don't care about any others the plugin registers - } - - public expose() { - // method is called by plugin that's being registered. - } -} diff --git a/packages/kbn-legacy-logging/src/log_events.ts b/packages/kbn-legacy-logging/src/log_events.ts deleted file mode 100644 index 193bfbea42ac..000000000000 --- a/packages/kbn-legacy-logging/src/log_events.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { ResponseObject } from '@hapi/hapi'; -import { EventData, isEventData } from './metadata'; - -export interface BaseEvent { - event: string; - timestamp: number; - pid: number; - tags?: string[]; -} - -export interface ResponseEvent extends BaseEvent { - event: 'response'; - method: 'GET' | 'POST' | 'PUT' | 'DELETE'; - statusCode: number; - path: string; - headers: Record; - responseHeaders: Record; - responsePayload: ResponseObject['source']; - responseTime: string; - query: Record; -} - -export interface OpsEvent extends BaseEvent { - event: 'ops'; - os: { - load: string[]; - }; - proc: Record; - load: string; -} - -export interface ErrorEvent extends BaseEvent { - event: 'error'; - error: Error; - url: string; -} - -export interface UndeclaredErrorEvent extends BaseEvent { - error: Error; -} - -export interface LogEvent extends BaseEvent { - data: EventData; -} - -export interface UnkownEvent extends BaseEvent { - data: string | Record; -} - -export type AnyEvent = - | ResponseEvent - | OpsEvent - | ErrorEvent - | UndeclaredErrorEvent - | LogEvent - | UnkownEvent; - -export const isResponseEvent = (e: AnyEvent): e is ResponseEvent => e.event === 'response'; -export const isOpsEvent = (e: AnyEvent): e is OpsEvent => e.event === 'ops'; -export const isErrorEvent = (e: AnyEvent): e is ErrorEvent => e.event === 'error'; -export const isLogEvent = (e: AnyEvent): e is LogEvent => isEventData((e as LogEvent).data); -export const isUndeclaredErrorEvent = (e: AnyEvent): e is UndeclaredErrorEvent => - (e as any).error instanceof Error; diff --git a/packages/kbn-legacy-logging/src/log_format.ts b/packages/kbn-legacy-logging/src/log_format.ts deleted file mode 100644 index a0eaf023dff1..000000000000 --- a/packages/kbn-legacy-logging/src/log_format.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Stream from 'stream'; -import moment from 'moment-timezone'; -import _ from 'lodash'; -import queryString from 'query-string'; -import numeral from '@elastic/numeral'; -import chalk from 'chalk'; -import { inspect } from 'util'; - -import { applyFiltersToKeys, getResponsePayloadBytes } from './utils'; -import { getLogEventData } from './metadata'; -import { LegacyLoggingConfig } from './schema'; -import { - AnyEvent, - ResponseEvent, - isResponseEvent, - isOpsEvent, - isErrorEvent, - isLogEvent, - isUndeclaredErrorEvent, -} from './log_events'; - -export type LogFormatConfig = Pick; - -function serializeError(err: any = {}) { - return { - message: err.message, - name: err.name, - stack: err.stack, - code: err.code, - signal: err.signal, - }; -} - -const levelColor = function (code: number) { - if (code < 299) return chalk.green(String(code)); - if (code < 399) return chalk.yellow(String(code)); - if (code < 499) return chalk.magentaBright(String(code)); - return chalk.red(String(code)); -}; - -export abstract class BaseLogFormat extends Stream.Transform { - constructor(private readonly config: LogFormatConfig) { - super({ - readableObjectMode: false, - writableObjectMode: true, - }); - } - - abstract format(data: Record): string; - - filter(data: Record) { - if (!this.config.filter) { - return data; - } - return applyFiltersToKeys(data, this.config.filter); - } - - _transform(event: AnyEvent, enc: string, next: Stream.TransformCallback) { - const data = this.filter(this.readEvent(event)); - this.push(this.format(data) + '\n'); - next(); - } - - getContentLength({ responsePayload, responseHeaders }: ResponseEvent): number | undefined { - try { - return getResponsePayloadBytes(responsePayload, responseHeaders); - } catch (e) { - // We intentionally swallow any errors as this information is - // only a nicety for logging purposes, and should not cause the - // server to crash if it cannot be determined. - this.push( - this.format({ - type: 'log', - tags: ['warning', 'logging'], - message: `Failed to calculate response payload bytes. [${e}]`, - }) + '\n' - ); - } - } - - extractAndFormatTimestamp(data: Record, format?: string) { - const { timezone } = this.config; - const date = moment(data['@timestamp']); - if (timezone) { - date.tz(timezone); - } - return date.format(format); - } - - readEvent(event: AnyEvent) { - const data: Record = { - type: event.event, - '@timestamp': event.timestamp, - tags: [...(event.tags || [])], - pid: event.pid, - }; - - if (isResponseEvent(event)) { - _.defaults(data, _.pick(event, ['method', 'statusCode'])); - - const source = _.get(event, 'source', {}); - data.req = { - url: event.path, - method: event.method || '', - headers: event.headers, - remoteAddress: source.remoteAddress, - userAgent: source.userAgent, - referer: source.referer, - }; - - data.res = { - statusCode: event.statusCode, - responseTime: event.responseTime, - contentLength: this.getContentLength(event), - }; - - const query = queryString.stringify(event.query, { sort: false }); - if (query) { - data.req.url += '?' + query; - } - - data.message = data.req.method.toUpperCase() + ' '; - data.message += data.req.url; - data.message += ' '; - data.message += levelColor(data.res.statusCode); - data.message += ' '; - data.message += chalk.gray(data.res.responseTime + 'ms'); - if (data.res.contentLength) { - data.message += chalk.gray(' - ' + numeral(data.res.contentLength).format('0.0b')); - } - } else if (isOpsEvent(event)) { - _.defaults(data, _.pick(event, ['pid', 'os', 'proc', 'load'])); - data.message = chalk.gray('memory: '); - data.message += numeral(_.get(data, 'proc.mem.heapUsed')).format('0.0b'); - data.message += ' '; - data.message += chalk.gray('uptime: '); - data.message += numeral(_.get(data, 'proc.uptime')).format('00:00:00'); - data.message += ' '; - data.message += chalk.gray('load: ['); - data.message += _.get(data, 'os.load', []) - .map((val: number) => { - return numeral(val).format('0.00'); - }) - .join(' '); - data.message += chalk.gray(']'); - data.message += ' '; - data.message += chalk.gray('delay: '); - data.message += numeral(_.get(data, 'proc.delay')).format('0.000'); - } else if (isErrorEvent(event)) { - data.level = 'error'; - data.error = serializeError(event.error); - data.url = event.url; - const message = _.get(event, 'error.message'); - data.message = message || 'Unknown error (no message)'; - } else if (isUndeclaredErrorEvent(event)) { - data.type = 'error'; - data.level = _.includes(event.tags, 'fatal') ? 'fatal' : 'error'; - data.error = serializeError(event.error); - const message = _.get(event, 'error.message'); - data.message = message || 'Unknown error object (no message)'; - } else if (isLogEvent(event)) { - _.assign(data, getLogEventData(event.data)); - } else { - data.message = _.isString(event.data) ? event.data : inspect(event.data); - } - return data; - } -} diff --git a/packages/kbn-legacy-logging/src/log_format_json.test.ts b/packages/kbn-legacy-logging/src/log_format_json.test.ts deleted file mode 100644 index 3255c5d17bb3..000000000000 --- a/packages/kbn-legacy-logging/src/log_format_json.test.ts +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import moment from 'moment'; - -import { attachMetaData } from './metadata'; -import { createListStream, createPromiseFromStreams } from '@kbn/utils'; -import { KbnLoggerJsonFormat } from './log_format_json'; - -const time = +moment('2010-01-01T05:15:59Z', moment.ISO_8601); - -const makeEvent = (eventType: string) => ({ - event: eventType, - timestamp: time, -}); - -describe('KbnLoggerJsonFormat', () => { - const config: any = {}; - - describe('event types and messages', () => { - let format: KbnLoggerJsonFormat; - beforeEach(() => { - format = new KbnLoggerJsonFormat(config); - }); - - it('log', async () => { - const result = await createPromiseFromStreams([ - createListStream([makeEvent('log')]), - format, - ]); - const { type, message } = JSON.parse(result); - - expect(type).toBe('log'); - expect(message).toBe('undefined'); - }); - - describe('response', () => { - it('handles a response object', async () => { - const event = { - ...makeEvent('response'), - statusCode: 200, - contentLength: 800, - responseTime: 12000, - method: 'GET', - path: '/path/to/resource', - responsePayload: '1234567879890', - source: { - remoteAddress: '127.0.0.1', - userAgent: 'Test Thing', - referer: 'elastic.co', - }, - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - const { type, method, statusCode, message, req } = JSON.parse(result); - - expect(type).toBe('response'); - expect(method).toBe('GET'); - expect(statusCode).toBe(200); - expect(message).toBe('GET /path/to/resource 200 12000ms - 13.0B'); - expect(req.remoteAddress).toBe('127.0.0.1'); - expect(req.userAgent).toBe('Test Thing'); - }); - - it('leaves payload size empty if not available', async () => { - const event = { - ...makeEvent('response'), - statusCode: 200, - responseTime: 12000, - method: 'GET', - path: '/path/to/resource', - responsePayload: null, - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - expect(JSON.parse(result).message).toBe('GET /path/to/resource 200 12000ms'); - }); - }); - - it('ops', async () => { - const event = { - ...makeEvent('ops'), - os: { - load: [1, 1, 2], - }, - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - const { type, message } = JSON.parse(result); - - expect(type).toBe('ops'); - expect(message).toBe('memory: 0.0B uptime: 0:00:00 load: [1.00 1.00 2.00] delay: 0.000'); - }); - - describe('with metadata', () => { - it('logs an event with meta data', async () => { - const event = { - data: attachMetaData('message for event', { - prop1: 'value1', - prop2: 'value2', - }), - tags: ['tag1', 'tag2'], - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - const { level, message, prop1, prop2, tags } = JSON.parse(result); - - expect(level).toBe(undefined); - expect(message).toBe('message for event'); - expect(prop1).toBe('value1'); - expect(prop2).toBe('value2'); - expect(tags).toEqual(['tag1', 'tag2']); - }); - - it('meta data rewrites event fields', async () => { - const event = { - data: attachMetaData('message for event', { - tags: ['meta-data-tag'], - prop1: 'value1', - prop2: 'value2', - }), - tags: ['tag1', 'tag2'], - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - const { level, message, prop1, prop2, tags } = JSON.parse(result); - - expect(level).toBe(undefined); - expect(message).toBe('message for event'); - expect(prop1).toBe('value1'); - expect(prop2).toBe('value2'); - expect(tags).toEqual(['meta-data-tag']); - }); - - it('logs an event with empty meta data', async () => { - const event = { - data: attachMetaData('message for event'), - tags: ['tag1', 'tag2'], - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - const { level, message, prop1, prop2, tags } = JSON.parse(result); - - expect(level).toBe(undefined); - expect(message).toBe('message for event'); - expect(prop1).toBe(undefined); - expect(prop2).toBe(undefined); - expect(tags).toEqual(['tag1', 'tag2']); - }); - - it('does not log meta data for an error event', async () => { - const event = { - error: new Error('reason'), - data: attachMetaData('message for event', { - prop1: 'value1', - prop2: 'value2', - }), - tags: ['tag1', 'tag2'], - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - const { level, message, prop1, prop2, tags } = JSON.parse(result); - - expect(level).toBe('error'); - expect(message).toBe('reason'); - expect(prop1).toBe(undefined); - expect(prop2).toBe(undefined); - expect(tags).toEqual(['tag1', 'tag2']); - }); - }); - - describe('errors', () => { - it('error type', async () => { - const event = { - ...makeEvent('error'), - error: { - message: 'test error 0', - }, - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - const { level, message, error } = JSON.parse(result); - - expect(level).toBe('error'); - expect(message).toBe('test error 0'); - expect(error).toEqual({ message: 'test error 0' }); - }); - - it('with no message', async () => { - const event = { - event: 'error', - error: {}, - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - const { level, message, error } = JSON.parse(result); - - expect(level).toBe('error'); - expect(message).toBe('Unknown error (no message)'); - expect(error).toEqual({}); - }); - - it('event error instanceof Error', async () => { - const event = { - error: new Error('test error 2') as any, - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - const { level, message, error } = JSON.parse(result); - - expect(level).toBe('error'); - expect(message).toBe('test error 2'); - - expect(error.message).toBe(event.error.message); - expect(error.name).toBe(event.error.name); - expect(error.stack).toBe(event.error.stack); - expect(error.code).toBe(event.error.code); - expect(error.signal).toBe(event.error.signal); - }); - - it('event error instanceof Error - fatal', async () => { - const event = { - error: new Error('test error 2') as any, - tags: ['fatal', 'tag2'], - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - const { tags, level, message, error } = JSON.parse(result); - - expect(tags).toEqual(['fatal', 'tag2']); - expect(level).toBe('fatal'); - expect(message).toBe('test error 2'); - - expect(error.message).toBe(event.error.message); - expect(error.name).toBe(event.error.name); - expect(error.stack).toBe(event.error.stack); - expect(error.code).toBe(event.error.code); - expect(error.signal).toBe(event.error.signal); - }); - - it('event error instanceof Error, no message', async () => { - const event = { - error: new Error('') as any, - }; - const result = await createPromiseFromStreams([createListStream([event]), format]); - const { level, message, error } = JSON.parse(result); - - expect(level).toBe('error'); - expect(message).toBe('Unknown error object (no message)'); - - expect(error.message).toBe(event.error.message); - expect(error.name).toBe(event.error.name); - expect(error.stack).toBe(event.error.stack); - expect(error.code).toBe(event.error.code); - expect(error.signal).toBe(event.error.signal); - }); - }); - }); - - describe('timezone', () => { - it('logs in UTC', async () => { - const format = new KbnLoggerJsonFormat({ - timezone: 'UTC', - } as any); - - const result = await createPromiseFromStreams([ - createListStream([makeEvent('log')]), - format, - ]); - - const { '@timestamp': timestamp } = JSON.parse(result); - expect(timestamp).toBe(moment.utc(time).format()); - }); - - it('logs in local timezone timezone is undefined', async () => { - const format = new KbnLoggerJsonFormat({} as any); - - const result = await createPromiseFromStreams([ - createListStream([makeEvent('log')]), - format, - ]); - - const { '@timestamp': timestamp } = JSON.parse(result); - expect(timestamp).toBe(moment(time).format()); - }); - }); -}); diff --git a/packages/kbn-legacy-logging/src/log_format_json.ts b/packages/kbn-legacy-logging/src/log_format_json.ts deleted file mode 100644 index 427415d1715a..000000000000 --- a/packages/kbn-legacy-logging/src/log_format_json.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// @ts-expect-error missing type def -import stringify from 'json-stringify-safe'; -import { BaseLogFormat } from './log_format'; - -const stripColors = function (string: string) { - return string.replace(/\u001b[^m]+m/g, ''); -}; - -export class KbnLoggerJsonFormat extends BaseLogFormat { - format(data: Record) { - data.message = stripColors(data.message); - data['@timestamp'] = this.extractAndFormatTimestamp(data); - return stringify(data); - } -} diff --git a/packages/kbn-legacy-logging/src/log_format_string.test.ts b/packages/kbn-legacy-logging/src/log_format_string.test.ts deleted file mode 100644 index 3ea02c2cfb28..000000000000 --- a/packages/kbn-legacy-logging/src/log_format_string.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import moment from 'moment'; - -import { attachMetaData } from './metadata'; -import { createListStream, createPromiseFromStreams } from '@kbn/utils'; -import { KbnLoggerStringFormat } from './log_format_string'; - -const time = +moment('2010-01-01T05:15:59Z', moment.ISO_8601); - -const makeEvent = () => ({ - event: 'log', - timestamp: time, - tags: ['tag'], - pid: 1, - data: 'my log message', -}); - -describe('KbnLoggerStringFormat', () => { - it('logs in UTC', async () => { - const format = new KbnLoggerStringFormat({ - timezone: 'UTC', - } as any); - - const result = await createPromiseFromStreams([createListStream([makeEvent()]), format]); - - expect(String(result)).toContain(moment.utc(time).format('HH:mm:ss.SSS')); - }); - - it('logs in local timezone when timezone is undefined', async () => { - const format = new KbnLoggerStringFormat({} as any); - - const result = await createPromiseFromStreams([createListStream([makeEvent()]), format]); - - expect(String(result)).toContain(moment(time).format('HH:mm:ss.SSS')); - }); - describe('with metadata', () => { - it('does not log meta data', async () => { - const format = new KbnLoggerStringFormat({} as any); - const event = { - data: attachMetaData('message for event', { - prop1: 'value1', - }), - tags: ['tag1', 'tag2'], - }; - - const result = await createPromiseFromStreams([createListStream([event]), format]); - - const resultString = String(result); - expect(resultString).toContain('tag1'); - expect(resultString).toContain('tag2'); - expect(resultString).toContain('message for event'); - - expect(resultString).not.toContain('value1'); - expect(resultString).not.toContain('prop1'); - }); - }); -}); diff --git a/packages/kbn-legacy-logging/src/log_format_string.ts b/packages/kbn-legacy-logging/src/log_format_string.ts deleted file mode 100644 index da21e56e0034..000000000000 --- a/packages/kbn-legacy-logging/src/log_format_string.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import chalk from 'chalk'; - -import { BaseLogFormat } from './log_format'; - -const statuses = ['err', 'info', 'error', 'warning', 'fatal', 'status', 'debug']; - -const typeColors: Record = { - log: 'white', - req: 'green', - res: 'green', - ops: 'cyan', - config: 'cyan', - err: 'red', - info: 'green', - error: 'red', - warning: 'red', - fatal: 'magentaBright', - status: 'yellowBright', - debug: 'gray', - server: 'gray', - optmzr: 'white', - manager: 'green', - optimize: 'magentaBright', - listening: 'magentaBright', - scss: 'magentaBright', -}; - -const color = _.memoize((name: string): ((...text: string[]) => string) => { - // @ts-expect-error couldn't even get rid of the error with an any cast - return chalk[typeColors[name]] || _.identity; -}); - -const type = _.memoize((t: string) => { - return color(t)(_.pad(t, 7).slice(0, 7)); -}); - -const prefix = process.env.isDevCliChild ? `${type('server')} ` : ''; - -export class KbnLoggerStringFormat extends BaseLogFormat { - format(data: Record) { - const time = color('time')(this.extractAndFormatTimestamp(data, 'HH:mm:ss.SSS')); - const msg = data.error ? color('error')(data.error.stack) : color('message')(data.message); - - const tags = _(data.tags) - .sortBy(function (tag) { - if (color(tag) === _.identity) return `2${tag}`; - if (_.includes(statuses, tag)) return `0${tag}`; - return `1${tag}`; - }) - .reduce(function (s, t) { - return s + `[${color(t)(t)}]`; - }, ''); - - return `${prefix}${type(data.type)} [${time}] ${tags} ${msg}`; - } -} diff --git a/packages/kbn-legacy-logging/src/log_interceptor.test.ts b/packages/kbn-legacy-logging/src/log_interceptor.test.ts deleted file mode 100644 index 53d622444ece..000000000000 --- a/packages/kbn-legacy-logging/src/log_interceptor.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ErrorEvent } from './log_events'; -import { LogInterceptor } from './log_interceptor'; - -function stubClientErrorEvent(errorMeta: Record): ErrorEvent { - const error = new Error(); - Object.assign(error, errorMeta); - return { - event: 'error', - url: '', - pid: 1234, - timestamp: Date.now(), - tags: ['connection', 'client', 'error'], - error, - }; -} - -const stubEconnresetEvent = () => stubClientErrorEvent({ code: 'ECONNRESET' }); -const stubEpipeEvent = () => stubClientErrorEvent({ errno: 'EPIPE' }); -const stubEcanceledEvent = () => stubClientErrorEvent({ errno: 'ECANCELED' }); - -function assertDowngraded(transformed: Record) { - expect(!!transformed).toBe(true); - expect(transformed).toHaveProperty('event', 'log'); - expect(transformed).toHaveProperty('tags'); - expect(transformed.tags).not.toContain('error'); -} - -describe('server logging LogInterceptor', () => { - describe('#downgradeIfEconnreset()', () => { - it('transforms ECONNRESET events', () => { - const interceptor = new LogInterceptor(); - const event = stubEconnresetEvent(); - assertDowngraded(interceptor.downgradeIfEconnreset(event)!); - }); - - it('does not match if the tags are not in order', () => { - const interceptor = new LogInterceptor(); - const event = stubEconnresetEvent(); - event.tags = [...event.tags!.slice(1), event.tags![0]]; - expect(interceptor.downgradeIfEconnreset(event)).toBe(null); - }); - - it('ignores non ECONNRESET events', () => { - const interceptor = new LogInterceptor(); - const event = stubClientErrorEvent({ errno: 'not ECONNRESET' }); - expect(interceptor.downgradeIfEconnreset(event)).toBe(null); - }); - - it('ignores if tags are wrong', () => { - const interceptor = new LogInterceptor(); - const event = stubEconnresetEvent(); - event.tags = ['different', 'tags']; - expect(interceptor.downgradeIfEconnreset(event)).toBe(null); - }); - }); - - describe('#downgradeIfEpipe()', () => { - it('transforms EPIPE events', () => { - const interceptor = new LogInterceptor(); - const event = stubEpipeEvent(); - assertDowngraded(interceptor.downgradeIfEpipe(event)!); - }); - - it('does not match if the tags are not in order', () => { - const interceptor = new LogInterceptor(); - const event = stubEpipeEvent(); - event.tags = [...event.tags!.slice(1), event.tags![0]]; - expect(interceptor.downgradeIfEpipe(event)).toBe(null); - }); - - it('ignores non EPIPE events', () => { - const interceptor = new LogInterceptor(); - const event = stubClientErrorEvent({ errno: 'not EPIPE' }); - expect(interceptor.downgradeIfEpipe(event)).toBe(null); - }); - - it('ignores if tags are wrong', () => { - const interceptor = new LogInterceptor(); - const event = stubEpipeEvent(); - event.tags = ['different', 'tags']; - expect(interceptor.downgradeIfEpipe(event)).toBe(null); - }); - }); - - describe('#downgradeIfEcanceled()', () => { - it('transforms ECANCELED events', () => { - const interceptor = new LogInterceptor(); - const event = stubEcanceledEvent(); - assertDowngraded(interceptor.downgradeIfEcanceled(event)!); - }); - - it('does not match if the tags are not in order', () => { - const interceptor = new LogInterceptor(); - const event = stubEcanceledEvent(); - event.tags = [...event.tags!.slice(1), event.tags![0]]; - expect(interceptor.downgradeIfEcanceled(event)).toBe(null); - }); - - it('ignores non ECANCELED events', () => { - const interceptor = new LogInterceptor(); - const event = stubClientErrorEvent({ errno: 'not ECANCELLED' }); - expect(interceptor.downgradeIfEcanceled(event)).toBe(null); - }); - - it('ignores if tags are wrong', () => { - const interceptor = new LogInterceptor(); - const event = stubEcanceledEvent(); - event.tags = ['different', 'tags']; - expect(interceptor.downgradeIfEcanceled(event)).toBe(null); - }); - }); - - describe('#downgradeIfHTTPSWhenHTTP', () => { - it('transforms https requests when serving http errors', () => { - const interceptor = new LogInterceptor(); - const event = stubClientErrorEvent({ message: 'Parse Error', code: 'HPE_INVALID_METHOD' }); - assertDowngraded(interceptor.downgradeIfHTTPSWhenHTTP(event)!); - }); - - it('ignores non events', () => { - const interceptor = new LogInterceptor(); - const event = stubClientErrorEvent({ - message: 'Parse Error', - code: 'NOT_HPE_INVALID_METHOD', - }); - expect(interceptor.downgradeIfEcanceled(event)).toBe(null); - }); - }); - - describe('#downgradeIfHTTPWhenHTTPS', () => { - it('transforms http requests when serving https errors', () => { - const message = - '4584650176:error:1408F09C:SSL routines:ssl3_get_record:http request:../deps/openssl/openssl/ssl/record/ssl3_record.c:322:\n'; - const interceptor = new LogInterceptor(); - const event = stubClientErrorEvent({ message }); - assertDowngraded(interceptor.downgradeIfHTTPWhenHTTPS(event)!); - }); - - it('ignores non events', () => { - const interceptor = new LogInterceptor(); - const event = stubClientErrorEvent({ message: 'Not error' }); - expect(interceptor.downgradeIfEcanceled(event)).toBe(null); - }); - }); -}); diff --git a/packages/kbn-legacy-logging/src/log_interceptor.ts b/packages/kbn-legacy-logging/src/log_interceptor.ts deleted file mode 100644 index 1085806135ca..000000000000 --- a/packages/kbn-legacy-logging/src/log_interceptor.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Stream from 'stream'; -import { get, isEqual } from 'lodash'; -import { AnyEvent } from './log_events'; - -/** - * Matches error messages when clients connect via HTTP instead of HTTPS; see unit test for full message. Warning: this can change when Node - * and its bundled OpenSSL binary are upgraded. - */ -const OPENSSL_GET_RECORD_REGEX = /ssl3_get_record:http/; - -function doTagsMatch(event: AnyEvent, tags: string[]) { - return isEqual(event.tags, tags); -} - -function doesMessageMatch(errorMessage: string, match: RegExp | string) { - if (!errorMessage) { - return false; - } - if (match instanceof RegExp) { - return match.test(errorMessage); - } - return errorMessage === match; -} - -// converts the given event into a debug log if it's an error of the given type -function downgradeIfErrorType(errorType: string, event: AnyEvent) { - const isClientError = doTagsMatch(event, ['connection', 'client', 'error']); - if (!isClientError) { - return null; - } - - const matchesErrorType = - get(event, 'error.code') === errorType || get(event, 'error.errno') === errorType; - if (!matchesErrorType) { - return null; - } - - const errorTypeTag = errorType.toLowerCase(); - - return { - event: 'log', - pid: event.pid, - timestamp: event.timestamp, - tags: ['debug', 'connection', errorTypeTag], - data: `${errorType}: Socket was closed by the client (probably the browser) before it could be read completely`, - }; -} - -function downgradeIfErrorMessage(match: RegExp | string, event: AnyEvent) { - const isClientError = doTagsMatch(event, ['connection', 'client', 'error']); - const errorMessage = get(event, 'error.message'); - const matchesErrorMessage = isClientError && doesMessageMatch(errorMessage, match); - - if (!matchesErrorMessage) { - return null; - } - - return { - event: 'log', - pid: event.pid, - timestamp: event.timestamp, - tags: ['debug', 'connection'], - data: errorMessage, - }; -} - -export class LogInterceptor extends Stream.Transform { - constructor() { - super({ - readableObjectMode: true, - writableObjectMode: true, - }); - } - - /** - * Since the upgrade to hapi 14, any socket read - * error is surfaced as a generic "client error" - * but "ECONNRESET" specifically is not useful for the - * logs unless you are trying to debug edge-case behaviors. - * - * For that reason, we downgrade this from error to debug level - * - * @param {object} - log event - */ - downgradeIfEconnreset(event: AnyEvent) { - return downgradeIfErrorType('ECONNRESET', event); - } - - /** - * Since the upgrade to hapi 14, any socket write - * error is surfaced as a generic "client error" - * but "EPIPE" specifically is not useful for the - * logs unless you are trying to debug edge-case behaviors. - * - * For that reason, we downgrade this from error to debug level - * - * @param {object} - log event - */ - downgradeIfEpipe(event: AnyEvent) { - return downgradeIfErrorType('EPIPE', event); - } - - /** - * Since the upgrade to hapi 14, any socket write - * error is surfaced as a generic "client error" - * but "ECANCELED" specifically is not useful for the - * logs unless you are trying to debug edge-case behaviors. - * - * For that reason, we downgrade this from error to debug level - * - * @param {object} - log event - */ - downgradeIfEcanceled(event: AnyEvent) { - return downgradeIfErrorType('ECANCELED', event); - } - - downgradeIfHTTPSWhenHTTP(event: AnyEvent) { - return downgradeIfErrorType('HPE_INVALID_METHOD', event); - } - - downgradeIfHTTPWhenHTTPS(event: AnyEvent) { - return downgradeIfErrorMessage(OPENSSL_GET_RECORD_REGEX, event); - } - - _transform(event: AnyEvent, enc: string, next: Stream.TransformCallback) { - const downgraded = - this.downgradeIfEconnreset(event) || - this.downgradeIfEpipe(event) || - this.downgradeIfEcanceled(event) || - this.downgradeIfHTTPSWhenHTTP(event) || - this.downgradeIfHTTPWhenHTTPS(event); - - this.push(downgraded || event); - next(); - } -} diff --git a/packages/kbn-legacy-logging/src/log_reporter.test.ts b/packages/kbn-legacy-logging/src/log_reporter.test.ts deleted file mode 100644 index a2ad8984ba24..000000000000 --- a/packages/kbn-legacy-logging/src/log_reporter.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import os from 'os'; -import path from 'path'; -import fs from 'fs'; - -import stripAnsi from 'strip-ansi'; - -import { getLogReporter } from './log_reporter'; - -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -describe('getLogReporter', () => { - it('should log to stdout (not json)', async () => { - const lines: string[] = []; - const origWrite = process.stdout.write; - process.stdout.write = (buffer: string | Uint8Array): boolean => { - lines.push(stripAnsi(buffer.toString()).trim()); - return true; - }; - - const loggerStream = getLogReporter({ - config: { - json: false, - dest: 'stdout', - filter: {}, - }, - events: { log: '*' }, - }); - - loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); - - await sleep(500); - - process.stdout.write = origWrite; - expect(lines.length).toBe(1); - expect(lines[0]).toMatch(/^log \[[^\]]*\] \[foo\] hello world$/); - }); - - it('should log to stdout (as json)', async () => { - const lines: string[] = []; - const origWrite = process.stdout.write; - process.stdout.write = (buffer: string | Uint8Array): boolean => { - lines.push(JSON.parse(buffer.toString().trim())); - return true; - }; - - const loggerStream = getLogReporter({ - config: { - json: true, - dest: 'stdout', - filter: {}, - }, - events: { log: '*' }, - }); - - loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); - - await sleep(500); - - process.stdout.write = origWrite; - expect(lines.length).toBe(1); - expect(lines[0]).toMatchObject({ - type: 'log', - tags: ['foo'], - message: 'hello world', - }); - }); - - it('should log to custom file (not json)', async () => { - const dir = os.tmpdir(); - const logfile = `dest-${Date.now()}.log`; - const dest = path.join(dir, logfile); - - const loggerStream = getLogReporter({ - config: { - json: false, - dest, - filter: {}, - }, - events: { log: '*' }, - }); - - loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); - - await sleep(500); - - const lines = stripAnsi(fs.readFileSync(dest, { encoding: 'utf8' })) - .trim() - .split(os.EOL); - expect(lines.length).toBe(1); - expect(lines[0]).toMatch(/^log \[[^\]]*\] \[foo\] hello world$/); - }); - - it('should log to custom file (as json)', async () => { - const dir = os.tmpdir(); - const logfile = `dest-${Date.now()}.log`; - const dest = path.join(dir, logfile); - - const loggerStream = getLogReporter({ - config: { - json: true, - dest, - filter: {}, - }, - events: { log: '*' }, - }); - - loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); - - await sleep(500); - - const lines = fs - .readFileSync(dest, { encoding: 'utf8' }) - .trim() - .split(os.EOL) - .map((data) => JSON.parse(data)); - expect(lines.length).toBe(1); - expect(lines[0]).toMatchObject({ - type: 'log', - tags: ['foo'], - message: 'hello world', - }); - }); -}); diff --git a/packages/kbn-legacy-logging/src/log_reporter.ts b/packages/kbn-legacy-logging/src/log_reporter.ts deleted file mode 100644 index d42fb78f1647..000000000000 --- a/packages/kbn-legacy-logging/src/log_reporter.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { createWriteStream } from 'fs'; -import { pipeline } from 'stream'; - -// @ts-expect-error missing type def -import { Squeeze } from '@hapi/good-squeeze'; - -import { KbnLoggerJsonFormat } from './log_format_json'; -import { KbnLoggerStringFormat } from './log_format_string'; -import { LogInterceptor } from './log_interceptor'; -import { LogFormatConfig } from './log_format'; - -export function getLogReporter({ events, config }: { events: any; config: LogFormatConfig }) { - const squeeze = new Squeeze(events); - const format = config.json ? new KbnLoggerJsonFormat(config) : new KbnLoggerStringFormat(config); - const logInterceptor = new LogInterceptor(); - - if (config.dest === 'stdout') { - pipeline(logInterceptor, squeeze, format, onFinished); - // The `pipeline` function is used to properly close all streams in the - // pipeline in case one of them ends or fails. Since stdout obviously - // shouldn't be closed in case of a failure in one of the other streams, - // we're not including that in the call to `pipeline`, but rely on the old - // `pipe` function instead. - format.pipe(process.stdout); - } else { - const dest = createWriteStream(config.dest, { - flags: 'a', - encoding: 'utf8', - }); - pipeline(logInterceptor, squeeze, format, dest, onFinished); - } - - return logInterceptor; -} - -function onFinished(err: NodeJS.ErrnoException | null) { - if (err) { - // eslint-disable-next-line no-console - console.error('An unexpected error occurred in the logging pipeline:', err.stack); - } -} diff --git a/packages/kbn-legacy-logging/src/metadata.ts b/packages/kbn-legacy-logging/src/metadata.ts deleted file mode 100644 index 0f41673ef672..000000000000 --- a/packages/kbn-legacy-logging/src/metadata.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { isPlainObject } from 'lodash'; - -export const metadataSymbol = Symbol('log message with metadata'); - -export interface EventData { - [metadataSymbol]?: EventMetadata; - [key: string]: any; -} - -export interface EventMetadata { - message: string; - metadata: Record; -} - -export const isEventData = (eventData: EventData) => { - return Boolean(isPlainObject(eventData) && eventData[metadataSymbol]); -}; - -export const getLogEventData = (eventData: EventData) => { - const { message, metadata } = eventData[metadataSymbol]!; - return { - ...metadata, - message, - }; -}; - -export const attachMetaData = (message: string, metadata: Record = {}) => { - return { - [metadataSymbol]: { - message, - metadata, - }, - }; -}; diff --git a/packages/kbn-legacy-logging/src/rotate/index.ts b/packages/kbn-legacy-logging/src/rotate/index.ts deleted file mode 100644 index 39305dcccf78..000000000000 --- a/packages/kbn-legacy-logging/src/rotate/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Server } from '@hapi/hapi'; -import { LogRotator } from './log_rotator'; -import { LegacyLoggingConfig } from '../schema'; - -let logRotator: LogRotator; - -export async function setupLoggingRotate(server: Server, config: LegacyLoggingConfig) { - // If log rotate is not enabled we skip - if (!config.rotate.enabled) { - return; - } - - // We don't want to run logging rotate server if - // we are not logging to a file - if (config.dest === 'stdout') { - server.log( - ['warning', 'logging:rotate'], - 'Log rotation is enabled but logging.dest is configured for stdout. Set logging.dest to a file for this setting to take effect.' - ); - return; - } - - // Enable Logging Rotate Service - // We need the master process and it can - // try to setupLoggingRotate more than once, - // so we'll need to assure it only loads once. - if (!logRotator) { - logRotator = new LogRotator(config, server); - await logRotator.start(); - } - - return logRotator; -} diff --git a/packages/kbn-legacy-logging/src/rotate/log_rotator.test.ts b/packages/kbn-legacy-logging/src/rotate/log_rotator.test.ts deleted file mode 100644 index ce9a24e63455..000000000000 --- a/packages/kbn-legacy-logging/src/rotate/log_rotator.test.ts +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import del from 'del'; -import fs, { existsSync, mkdirSync, statSync, writeFileSync } from 'fs'; -import { tmpdir } from 'os'; -import { dirname, join } from 'path'; -import { LogRotator } from './log_rotator'; -import { LegacyLoggingConfig } from '../schema'; - -const mockOn = jest.fn(); -jest.mock('chokidar', () => ({ - watch: jest.fn(() => ({ - on: mockOn, - close: jest.fn(), - })), -})); - -jest.mock('lodash', () => ({ - ...(jest.requireActual('lodash') as any), - throttle: (fn: any) => fn, -})); - -const tempDir = join(tmpdir(), 'kbn_log_rotator_test'); -const testFilePath = join(tempDir, 'log_rotator_test_log_file.log'); - -const createLogRotatorConfig = (logFilePath: string): LegacyLoggingConfig => { - return { - dest: logFilePath, - rotate: { - enabled: true, - keepFiles: 2, - everyBytes: 2, - usePolling: false, - pollingInterval: 10000, - pollingPolicyTestTimeout: 4000, - }, - } as LegacyLoggingConfig; -}; - -const mockServer: any = { - log: jest.fn(), -}; - -const writeBytesToFile = (filePath: string, numberOfBytes: number) => { - writeFileSync(filePath, 'a'.repeat(numberOfBytes), { flag: 'a' }); -}; - -describe('LogRotator', () => { - beforeEach(() => { - mkdirSync(tempDir, { recursive: true }); - writeFileSync(testFilePath, ''); - }); - - afterEach(() => { - del.sync(tempDir, { force: true }); - mockOn.mockClear(); - }); - - it('rotates log file when bigger than set limit on start', async () => { - writeBytesToFile(testFilePath, 3); - - const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer); - jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {}); - - await logRotator.start(); - - expect(logRotator.running).toBe(true); - - await logRotator.stop(); - - expect(existsSync(join(tempDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy(); - }); - - it('rotates log file when equal than set limit over time', async () => { - writeBytesToFile(testFilePath, 1); - - const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer); - jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {}); - await logRotator.start(); - - expect(logRotator.running).toBe(true); - - const testLogFileDir = dirname(testFilePath); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeFalsy(); - - writeBytesToFile(testFilePath, 1); - - // ['change', [asyncFunction]] - const onChangeCb = mockOn.mock.calls[0][1]; - await onChangeCb(testLogFileDir, { size: 2 }); - - await logRotator.stop(); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy(); - }); - - it('rotates log file when file size is bigger than limit', async () => { - writeBytesToFile(testFilePath, 1); - - const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer); - jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {}); - await logRotator.start(); - - expect(logRotator.running).toBe(true); - - const testLogFileDir = dirname(testFilePath); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeFalsy(); - - writeBytesToFile(testFilePath, 2); - - // ['change', [asyncFunction]] - const onChangeCb = mockOn.mock.calls[0][1]; - await onChangeCb(testLogFileDir, { size: 3 }); - - await logRotator.stop(); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy(); - }); - - it('rotates log file service correctly keeps number of files', async () => { - writeBytesToFile(testFilePath, 3); - - const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer); - jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {}); - await logRotator.start(); - - expect(logRotator.running).toBe(true); - - const testLogFileDir = dirname(testFilePath); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy(); - - writeBytesToFile(testFilePath, 2); - - // ['change', [asyncFunction]] - const onChangeCb = mockOn.mock.calls[0][1]; - await onChangeCb(testLogFileDir, { size: 2 }); - - writeBytesToFile(testFilePath, 5); - await onChangeCb(testLogFileDir, { size: 5 }); - - await logRotator.stop(); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy(); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.1'))).toBeTruthy(); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.2'))).toBeFalsy(); - expect(statSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0')).size).toBe(5); - }); - - it('rotates log file service correctly keeps number of files even when number setting changes', async () => { - writeBytesToFile(testFilePath, 3); - - const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer); - jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {}); - await logRotator.start(); - - expect(logRotator.running).toBe(true); - - const testLogFileDir = dirname(testFilePath); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy(); - - writeBytesToFile(testFilePath, 2); - - // ['change', [asyncFunction]] - const onChangeCb = mockOn.mock.calls[0][1]; - await onChangeCb(testLogFileDir, { size: 2 }); - - writeBytesToFile(testFilePath, 5); - await onChangeCb(testLogFileDir, { size: 5 }); - - await logRotator.stop(); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy(); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.1'))).toBeTruthy(); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.2'))).toBeFalsy(); - expect(statSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0')).size).toBe(5); - - logRotator.keepFiles = 1; - await logRotator.start(); - - writeBytesToFile(testFilePath, 5); - await onChangeCb(testLogFileDir, { size: 5 }); - - await logRotator.stop(); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy(); - expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.1'))).toBeFalsy(); - expect(statSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0')).size).toBe(5); - }); - - it('rotates log file service correctly detects usePolling when it should be false', async () => { - writeBytesToFile(testFilePath, 1); - - const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer); - jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {}); - await logRotator.start(); - - expect(logRotator.running).toBe(true); - expect(logRotator.usePolling).toBe(false); - - const shouldUsePolling = await logRotator._shouldUsePolling(); - expect(shouldUsePolling).toBe(false); - - await logRotator.stop(); - }); - - it('rotates log file service correctly detects usePolling when it should be true', async () => { - writeBytesToFile(testFilePath, 1); - - const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer); - jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {}); - - jest.spyOn(fs, 'watch').mockImplementation( - () => - ({ - on: jest.fn((eventType, cb) => { - if (eventType === 'error') { - cb(); - } - }), - close: jest.fn(), - } as any) - ); - - await logRotator.start(); - - expect(logRotator.running).toBe(true); - expect(logRotator.usePolling).toBe(false); - expect(logRotator.shouldUsePolling).toBe(true); - - await logRotator.stop(); - }); - - it('rotates log file service correctly fallback to usePolling true after defined timeout', async () => { - jest.useFakeTimers(); - writeBytesToFile(testFilePath, 1); - - const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer); - jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {}); - jest.spyOn(fs, 'watch').mockImplementation( - () => - ({ - on: jest.fn((ev: string) => { - if (ev === 'error') { - jest.runTimersToTime(15000); - } - }), - close: jest.fn(), - } as any) - ); - - await logRotator.start(); - - expect(logRotator.running).toBe(true); - expect(logRotator.usePolling).toBe(false); - expect(logRotator.shouldUsePolling).toBe(true); - - await logRotator.stop(); - jest.useRealTimers(); - }); -}); diff --git a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts deleted file mode 100644 index 4b1e34839030..000000000000 --- a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as chokidar from 'chokidar'; -import fs from 'fs'; -import { Server } from '@hapi/hapi'; -import { throttle } from 'lodash'; -import { tmpdir } from 'os'; -import { basename, dirname, join, sep } from 'path'; -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; -import { promisify } from 'util'; -import { LegacyLoggingConfig } from '../schema'; - -const mkdirAsync = promisify(fs.mkdir); -const readdirAsync = promisify(fs.readdir); -const renameAsync = promisify(fs.rename); -const statAsync = promisify(fs.stat); -const unlinkAsync = promisify(fs.unlink); -const writeFileAsync = promisify(fs.writeFile); - -export class LogRotator { - private readonly config: LegacyLoggingConfig; - private readonly log: Server['log']; - public logFilePath: string; - public everyBytes: number; - public keepFiles: number; - public running: boolean; - private logFileSize: number; - public isRotating: boolean; - public throttledRotate: () => void; - public stalker: chokidar.FSWatcher | null; - public usePolling: boolean; - public pollingInterval: number; - private stalkerUsePollingPolicyTestTimeout: NodeJS.Timeout | null; - public shouldUsePolling: boolean; - - constructor(config: LegacyLoggingConfig, server: Server) { - this.config = config; - this.log = server.log.bind(server); - this.logFilePath = config.dest; - this.everyBytes = config.rotate.everyBytes; - this.keepFiles = config.rotate.keepFiles; - this.running = false; - this.logFileSize = 0; - this.isRotating = false; - this.throttledRotate = throttle(async () => await this._rotate(), 5000); - this.stalker = null; - this.usePolling = config.rotate.usePolling; - this.pollingInterval = config.rotate.pollingInterval; - this.shouldUsePolling = false; - this.stalkerUsePollingPolicyTestTimeout = null; - } - - async start() { - if (this.running) { - return; - } - - this.running = true; - - // create exit listener for cleanup purposes - this._createExitListener(); - - // call rotate on startup - await this._callRotateOnStartup(); - - // init log file size monitor - await this._startLogFileSizeMonitor(); - } - - stop = () => { - if (!this.running) { - return; - } - - // cleanup exit listener - this._deleteExitListener(); - - // stop log file size monitor - this._stopLogFileSizeMonitor(); - - this.running = false; - }; - - async _shouldUsePolling() { - try { - // Setup a test file in order to try the fs env - // and understand if we need to usePolling or not - const tempFileDir = tmpdir(); - const tempFile = join(tempFileDir, 'kbn_log_rotation_use_polling_test_file.log'); - - await mkdirAsync(tempFileDir, { recursive: true }); - await writeFileAsync(tempFile, ''); - - // setup fs.watch for the temp test file - const testWatcher = fs.watch(tempFile, { persistent: false }); - - // await writeFileAsync(tempFile, 'test'); - - const usePollingTest$ = new Observable((observer) => { - // observable complete function - const completeFn = (completeStatus: boolean) => { - if (this.stalkerUsePollingPolicyTestTimeout) { - clearTimeout(this.stalkerUsePollingPolicyTestTimeout); - } - testWatcher.close(); - - observer.next(completeStatus); - observer.complete(); - }; - - // setup conditions that would fire the observable - this.stalkerUsePollingPolicyTestTimeout = setTimeout( - () => completeFn(true), - this.config.rotate.pollingPolicyTestTimeout || 15000 - ); - testWatcher.on('change', () => completeFn(false)); - testWatcher.on('error', () => completeFn(true)); - - // fire test watcher events - setTimeout(() => { - fs.writeFileSync(tempFile, 'test'); - }, 0); - }); - - // wait for the first observable result and consider it as the result - // for our use polling test - const usePollingTestResult = await usePollingTest$.pipe(first()).toPromise(); - - // delete the temp file used for the test - await unlinkAsync(tempFile); - - return usePollingTestResult; - } catch { - return true; - } - } - - async _startLogFileSizeMonitor() { - this.usePolling = this.config.rotate.usePolling; - this.shouldUsePolling = await this._shouldUsePolling(); - - if (this.usePolling && !this.shouldUsePolling) { - this.log( - ['warning', 'logging:rotate'], - 'Looks like your current environment support a faster algorithm than polling. You can try to disable `usePolling`' - ); - } - - if (!this.usePolling && this.shouldUsePolling) { - this.log( - ['error', 'logging:rotate'], - 'Looks like within your current environment you need to use polling in order to enable log rotator. Please enable `usePolling`' - ); - } - - this.stalker = chokidar.watch(this.logFilePath, { - ignoreInitial: true, - awaitWriteFinish: false, - useFsEvents: false, - usePolling: this.usePolling, - interval: this.pollingInterval, - binaryInterval: this.pollingInterval, - alwaysStat: true, - atomic: false, - }); - this.stalker.on('change', this._logFileSizeMonitorHandler); - } - - _logFileSizeMonitorHandler = async (filename: string, stats: fs.Stats) => { - if (!filename || !stats) { - return; - } - - this.logFileSize = stats.size || 0; - await this.throttledRotate(); - }; - - _stopLogFileSizeMonitor() { - if (!this.stalker) { - return; - } - - this.stalker.close(); - - if (this.stalkerUsePollingPolicyTestTimeout) { - clearTimeout(this.stalkerUsePollingPolicyTestTimeout); - } - } - - _createExitListener() { - process.on('exit', this.stop); - } - - _deleteExitListener() { - process.removeListener('exit', this.stop); - } - - async _getLogFileSizeAndCreateIfNeeded() { - try { - const logFileStats = await statAsync(this.logFilePath); - return logFileStats.size; - } catch { - // touch the file to make the watcher being able to register - // change events - await writeFileAsync(this.logFilePath, ''); - return 0; - } - } - - async _callRotateOnStartup() { - this.logFileSize = await this._getLogFileSizeAndCreateIfNeeded(); - await this._rotate(); - } - - _shouldRotate() { - // should rotate evaluation - // 1. should rotate if current log size exceeds - // the defined one on everyBytes - // 2. should not rotate if is already rotating or if any - // of the conditions on 1. do not apply - if (this.isRotating) { - return false; - } - - return this.logFileSize >= this.everyBytes; - } - - async _rotate() { - if (!this._shouldRotate()) { - return; - } - - await this._rotateNow(); - } - - async _rotateNow() { - // rotate process - // 1. get rotated files metadata (list of log rotated files present on the log folder, numerical sorted) - // 2. delete last file - // 3. rename all files to the correct index +1 - // 4. rename + compress current log into 1 - // 5. send SIGHUP to reload log config - - // rotate process is starting - this.isRotating = true; - - // get rotated files metadata - const foundRotatedFiles = await this._readRotatedFilesMetadata(); - - // delete number of rotated files exceeding the keepFiles limit setting - const rotatedFiles: string[] = await this._deleteFoundRotatedFilesAboveKeepFilesLimit( - foundRotatedFiles - ); - - // delete last file - await this._deleteLastRotatedFile(rotatedFiles); - - // rename all files to correct index + 1 - // and normalize numbering if by some reason - // (for example log file deletion) that numbering - // was interrupted - await this._renameRotatedFilesByOne(rotatedFiles); - - // rename current log into 0 - await this._rotateCurrentLogFile(); - - // send SIGHUP to reload log configuration - this._sendReloadLogConfigSignal(); - - // Reset log file size - this.logFileSize = 0; - - // rotate process is finished - this.isRotating = false; - } - - async _readRotatedFilesMetadata() { - const logFileBaseName = basename(this.logFilePath); - const logFilesFolder = dirname(this.logFilePath); - const foundLogFiles: string[] = await readdirAsync(logFilesFolder); - - return ( - foundLogFiles - .filter((file) => new RegExp(`${logFileBaseName}\\.\\d`).test(file)) - // we use .slice(-1) here in order to retrieve the last number match in the read filenames - .sort((a, b) => Number(a.match(/(\d+)/g)!.slice(-1)) - Number(b.match(/(\d+)/g)!.slice(-1))) - .map((filename) => `${logFilesFolder}${sep}${filename}`) - ); - } - - async _deleteFoundRotatedFilesAboveKeepFilesLimit(foundRotatedFiles: string[]) { - if (foundRotatedFiles.length <= this.keepFiles) { - return foundRotatedFiles; - } - - const finalRotatedFiles = foundRotatedFiles.slice(0, this.keepFiles); - const rotatedFilesToDelete = foundRotatedFiles.slice( - finalRotatedFiles.length, - foundRotatedFiles.length - ); - - await Promise.all( - rotatedFilesToDelete.map((rotatedFilePath: string) => unlinkAsync(rotatedFilePath)) - ); - - return finalRotatedFiles; - } - - async _deleteLastRotatedFile(rotatedFiles: string[]) { - if (rotatedFiles.length < this.keepFiles) { - return; - } - - const lastFilePath: string = rotatedFiles.pop() as string; - await unlinkAsync(lastFilePath); - } - - async _renameRotatedFilesByOne(rotatedFiles: string[]) { - const logFileBaseName = basename(this.logFilePath); - const logFilesFolder = dirname(this.logFilePath); - - for (let i = rotatedFiles.length - 1; i >= 0; i--) { - const oldFilePath = rotatedFiles[i]; - const newFilePath = `${logFilesFolder}${sep}${logFileBaseName}.${i + 1}`; - await renameAsync(oldFilePath, newFilePath); - } - } - - async _rotateCurrentLogFile() { - const newFilePath = `${this.logFilePath}.0`; - await renameAsync(this.logFilePath, newFilePath); - } - - _sendReloadLogConfigSignal() { - if (!process.env.isDevCliChild || !process.send) { - process.emit('SIGHUP', 'SIGHUP'); - return; - } - - // Send a special message to the cluster manager - // so it can forward it correctly - // It will only run when we are under cluster mode (not under a production environment) - process.send(['RELOAD_LOGGING_CONFIG_FROM_SERVER_WORKER']); - } -} diff --git a/packages/kbn-legacy-logging/src/schema.ts b/packages/kbn-legacy-logging/src/schema.ts deleted file mode 100644 index 0330708e746c..000000000000 --- a/packages/kbn-legacy-logging/src/schema.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { schema } from '@kbn/config-schema'; - -/** - * @deprecated - * - * Legacy logging has been deprecated and will be removed in 8.0. - * Set up logging from the platform logging instead - */ -export interface LegacyLoggingConfig { - silent: boolean; - quiet: boolean; - verbose: boolean; - events: Record; - dest: string; - filter: Record; - json: boolean; - timezone?: string; - rotate: { - enabled: boolean; - everyBytes: number; - keepFiles: number; - pollingInterval: number; - usePolling: boolean; - pollingPolicyTestTimeout?: number; - }; -} - -export const legacyLoggingConfigSchema = schema.object({ - silent: schema.boolean({ defaultValue: false }), - quiet: schema.conditional( - schema.siblingRef('silent'), - true, - schema.boolean({ - defaultValue: true, - validate: (quiet) => { - if (!quiet) { - return 'must be true when `silent` is true'; - } - }, - }), - schema.boolean({ defaultValue: false }) - ), - verbose: schema.conditional( - schema.siblingRef('quiet'), - true, - schema.boolean({ - defaultValue: false, - validate: (verbose) => { - if (verbose) { - return 'must be false when `quiet` is true'; - } - }, - }), - schema.boolean({ defaultValue: false }) - ), - events: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), - dest: schema.string({ defaultValue: 'stdout' }), - filter: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), - json: schema.conditional( - schema.siblingRef('dest'), - 'stdout', - schema.boolean({ - defaultValue: !process.stdout.isTTY, - }), - schema.boolean({ - defaultValue: true, - }) - ), - timezone: schema.maybe(schema.string()), - rotate: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - everyBytes: schema.number({ - min: 1048576, // > 1MB - max: 1073741825, // < 1GB - defaultValue: 10485760, // 10MB - }), - keepFiles: schema.number({ - min: 2, - max: 1024, - defaultValue: 7, - }), - pollingInterval: schema.number({ - min: 5000, - max: 3600000, - defaultValue: 10000, - }), - usePolling: schema.boolean({ defaultValue: false }), - }), -}); diff --git a/packages/kbn-legacy-logging/src/setup_logging.test.ts b/packages/kbn-legacy-logging/src/setup_logging.test.ts deleted file mode 100644 index 8e1d76477f64..000000000000 --- a/packages/kbn-legacy-logging/src/setup_logging.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Server } from '@hapi/hapi'; -import { reconfigureLogging, setupLogging } from './setup_logging'; -import { LegacyLoggingConfig } from './schema'; - -describe('reconfigureLogging', () => { - test(`doesn't throw an error`, () => { - const server = new Server(); - const config: LegacyLoggingConfig = { - silent: false, - quiet: false, - verbose: true, - events: {}, - dest: '/tmp/foo', - filter: {}, - json: true, - rotate: { - enabled: false, - everyBytes: 0, - keepFiles: 0, - pollingInterval: 0, - usePolling: false, - }, - }; - setupLogging(server, config, 10); - reconfigureLogging(server, { ...config, dest: '/tmp/bar' }, 0); - }); -}); diff --git a/packages/kbn-legacy-logging/src/setup_logging.ts b/packages/kbn-legacy-logging/src/setup_logging.ts deleted file mode 100644 index a045469e8125..000000000000 --- a/packages/kbn-legacy-logging/src/setup_logging.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// @ts-expect-error missing typedef -import { plugin as good } from '@elastic/good'; -import { Server } from '@hapi/hapi'; -import { LegacyLoggingConfig } from './schema'; -import { getLoggingConfiguration } from './get_logging_config'; - -export async function setupLogging( - server: Server, - config: LegacyLoggingConfig, - opsInterval: number -) { - // NOTE: legacy logger creates a new stream for each new access - // In https://github.com/elastic/kibana/pull/55937 we reach the max listeners - // default limit of 10 for process.stdout which starts a long warning/error - // 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(60); - - return await server.register({ - plugin: good, - options: getLoggingConfiguration(config, opsInterval), - }); -} - -export function reconfigureLogging( - server: Server, - config: LegacyLoggingConfig, - opsInterval: number -) { - const loggingOptions = getLoggingConfiguration(config, opsInterval); - (server.plugins as any).good.reconfigure(loggingOptions); -} diff --git a/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.test.ts b/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.test.ts deleted file mode 100644 index b662c88eba7b..000000000000 --- a/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { applyFiltersToKeys } from './apply_filters_to_keys'; - -describe('applyFiltersToKeys(obj, actionsByKey)', function () { - it('applies for each key+prop in actionsByKey', function () { - const data = applyFiltersToKeys( - { - a: { - b: { - c: 1, - }, - d: { - e: 'foobar', - }, - }, - req: { - headers: { - authorization: 'Basic dskd939k2i', - }, - }, - }, - { - b: 'remove', - e: 'censor', - authorization: '/([^\\s]+)$/', - } - ); - - expect(data).toEqual({ - a: { - d: { - e: 'XXXXXX', - }, - }, - req: { - headers: { - authorization: 'Basic XXXXXXXXXX', - }, - }, - }); - }); -}); diff --git a/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.ts b/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.ts deleted file mode 100644 index 578fa3a83512..000000000000 --- a/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -function toPojo(obj: Record) { - return JSON.parse(JSON.stringify(obj)); -} - -function replacer(match: string, group: any[]) { - return new Array(group.length + 1).join('X'); -} - -function apply(obj: Record, key: string, action: string) { - for (const k in obj) { - if (obj.hasOwnProperty(k)) { - let val = obj[k]; - if (k === key) { - if (action === 'remove') { - delete obj[k]; - } else if (action === 'censor' && typeof val === 'object') { - delete obj[key]; - } else if (action === 'censor') { - obj[k] = ('' + val).replace(/./g, 'X'); - } else if (/\/.+\//.test(action)) { - const matches = action.match(/\/(.+)\//); - if (matches) { - const regex = new RegExp(matches[1]); - obj[k] = ('' + val).replace(regex, replacer); - } - } - } else if (typeof val === 'object') { - val = apply(val as Record, key, action); - } - } - } - return obj; -} - -export function applyFiltersToKeys( - obj: Record, - actionsByKey: Record -) { - return Object.keys(actionsByKey).reduce((output, key) => { - return apply(output, key, actionsByKey[key]); - }, toPojo(obj)); -} diff --git a/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts b/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts deleted file mode 100644 index 01d2cf29758d..000000000000 --- a/packages/kbn-legacy-logging/src/utils/get_payload_size.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import mockFs from 'mock-fs'; -import { createReadStream } from 'fs'; -import { PassThrough } from 'stream'; -import { createGzip, createGunzip } from 'zlib'; - -import { getResponsePayloadBytes } from './get_payload_size'; - -describe('getPayloadSize', () => { - describe('handles Buffers', () => { - test('with ascii characters', () => { - const payload = 'heya'; - const result = getResponsePayloadBytes(Buffer.from(payload)); - expect(result).toBe(4); - }); - - test('with special characters', () => { - const payload = '¡hola!'; - const result = getResponsePayloadBytes(Buffer.from(payload)); - expect(result).toBe(7); - }); - }); - - describe('handles streams', () => { - afterEach(() => mockFs.restore()); - - test('ignores streams that are not fs or zlib streams', async () => { - const result = getResponsePayloadBytes(new PassThrough()); - expect(result).toBe(undefined); - }); - - describe('fs streams', () => { - test('with ascii characters', async () => { - mockFs({ 'test.txt': 'heya' }); - const readStream = createReadStream('test.txt'); - - let data = ''; - for await (const chunk of readStream) { - data += chunk; - } - - const result = getResponsePayloadBytes(readStream); - expect(result).toBe(Buffer.byteLength(data)); - }); - - test('with special characters', async () => { - mockFs({ 'test.txt': '¡hola!' }); - const readStream = createReadStream('test.txt'); - - let data = ''; - for await (const chunk of readStream) { - data += chunk; - } - - const result = getResponsePayloadBytes(readStream); - expect(result).toBe(Buffer.byteLength(data)); - }); - - describe('zlib streams', () => { - test('with ascii characters', async () => { - mockFs({ 'test.txt': 'heya' }); - const readStream = createReadStream('test.txt'); - const source = readStream.pipe(createGzip()).pipe(createGunzip()); - - let data = ''; - for await (const chunk of source) { - data += chunk; - } - - const result = getResponsePayloadBytes(source); - - expect(data).toBe('heya'); - expect(result).toBe(source.bytesWritten); - }); - - test('with special characters', async () => { - mockFs({ 'test.txt': '¡hola!' }); - const readStream = createReadStream('test.txt'); - const source = readStream.pipe(createGzip()).pipe(createGunzip()); - - let data = ''; - for await (const chunk of source) { - data += chunk; - } - - const result = getResponsePayloadBytes(source); - - expect(data).toBe('¡hola!'); - expect(result).toBe(source.bytesWritten); - }); - }); - }); - }); - - describe('handles plain responses', () => { - test('when source is text', () => { - const result = getResponsePayloadBytes('heya'); - expect(result).toBe(4); - }); - - test('when source contains special characters', () => { - const result = getResponsePayloadBytes('¡hola!'); - expect(result).toBe(7); - }); - - test('when source is object', () => { - const payload = { message: 'heya' }; - const result = getResponsePayloadBytes(payload); - expect(result).toBe(JSON.stringify(payload).length); - }); - - test('when source is array object', () => { - const payload = [{ message: 'hey' }, { message: 'ya' }]; - const result = getResponsePayloadBytes(payload); - expect(result).toBe(JSON.stringify(payload).length); - }); - - test('returns undefined when source is not plain object', () => { - class TestClass { - constructor() {} - } - const result = getResponsePayloadBytes(new TestClass()); - expect(result).toBe(undefined); - }); - }); - - describe('handles content-length header', () => { - test('always provides content-length header if available', () => { - const headers = { 'content-length': '123' }; - const result = getResponsePayloadBytes('heya', headers); - expect(result).toBe(123); - }); - - test('uses first value when hapi header is an array', () => { - const headers = { 'content-length': ['123', '456'] }; - const result = getResponsePayloadBytes(null, headers); - expect(result).toBe(123); - }); - - test('returns undefined if length is NaN', () => { - const headers = { 'content-length': 'oops' }; - const result = getResponsePayloadBytes(null, headers); - expect(result).toBeUndefined(); - }); - }); - - test('defaults to undefined', () => { - const result = getResponsePayloadBytes(null); - expect(result).toBeUndefined(); - }); -}); diff --git a/packages/kbn-legacy-logging/src/utils/get_payload_size.ts b/packages/kbn-legacy-logging/src/utils/get_payload_size.ts deleted file mode 100644 index acc517c74c2d..000000000000 --- a/packages/kbn-legacy-logging/src/utils/get_payload_size.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { isPlainObject } from 'lodash'; -import { ReadStream } from 'fs'; -import { Zlib } from 'zlib'; -import type { ResponseObject } from '@hapi/hapi'; - -const isBuffer = (obj: unknown): obj is Buffer => Buffer.isBuffer(obj); -const isFsReadStream = (obj: unknown): obj is ReadStream => - typeof obj === 'object' && obj !== null && 'bytesRead' in obj && obj instanceof ReadStream; -const isZlibStream = (obj: unknown): obj is Zlib => { - return typeof obj === 'object' && obj !== null && 'bytesWritten' in obj; -}; -const isString = (obj: unknown): obj is string => typeof obj === 'string'; - -/** - * Attempts to determine the size (in bytes) of a hapi/good - * responsePayload based on the payload type. Falls back to - * `undefined` if the size cannot be determined. - * - * This is similar to the implementation in `core/server/http/logging`, - * however it uses more duck typing as we do not have access to the - * entire hapi request object like we do in the HttpServer. - * - * @param headers responseHeaders from hapi/good event - * @param payload responsePayload from hapi/good event - * - * @internal - */ -export function getResponsePayloadBytes( - payload: ResponseObject['source'], - headers: Record = {} -): number | undefined { - const contentLength = headers['content-length']; - if (contentLength) { - const val = parseInt( - // hapi response headers can be `string | string[]`, so we need to handle both cases - Array.isArray(contentLength) ? String(contentLength) : contentLength, - 10 - ); - return !isNaN(val) ? val : undefined; - } - - if (isBuffer(payload)) { - return payload.byteLength; - } - - if (isFsReadStream(payload)) { - return payload.bytesRead; - } - - if (isZlibStream(payload)) { - return payload.bytesWritten; - } - - if (isString(payload)) { - return Buffer.byteLength(payload); - } - - if (isPlainObject(payload) || Array.isArray(payload)) { - return Buffer.byteLength(JSON.stringify(payload)); - } - - return undefined; -} diff --git a/packages/kbn-legacy-logging/tsconfig.json b/packages/kbn-legacy-logging/tsconfig.json deleted file mode 100644 index 55047dbcadc9..000000000000 --- a/packages/kbn-legacy-logging/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../tsconfig.bazel.json", - "compilerOptions": { - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true, - "outDir": "target_types", - "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-legacy-logging/src", - "stripInternal": false, - "types": ["jest", "node"] - }, - "include": ["src/**/*"] -} diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 8fb31b36f39d..a0ca88e4e04b 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -116,3 +116,4 @@ pageLoadAssetSize: expressions: 239290 securitySolution: 231753 customIntegrations: 28810 + dataViews: 42000 diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index c96a1eb28cfc..cab1f6d916f0 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -9014,6 +9014,7 @@ class CiStatsReporter { const upstreamBranch = (_options$upstreamBran = options.upstreamBranch) !== null && _options$upstreamBran !== void 0 ? _options$upstreamBran : this.getUpstreamBranch(); const kibanaUuid = options.kibanaUuid === undefined ? this.getKibanaUuid() : options.kibanaUuid; let email; + let branch; try { const { @@ -9024,16 +9025,32 @@ class CiStatsReporter { this.log.debug(e.message); } + try { + const { + stdout + } = await (0, _execa.default)('git', ['branch', '--show-current']); + branch = stdout; + } catch (e) { + this.log.debug(e.message); + } + + const memUsage = process.memoryUsage(); const isElasticCommitter = email && email.endsWith('@elastic.co') ? true : false; const defaultMetadata = { + kibanaUuid, + isElasticCommitter, committerHash: email ? _crypto.default.createHash('sha256').update(email).digest('hex').substring(0, 20) : undefined, + email: isElasticCommitter ? email : undefined, + branch: isElasticCommitter ? branch : undefined, cpuCount: (_Os$cpus = _os.default.cpus()) === null || _Os$cpus === void 0 ? void 0 : _Os$cpus.length, cpuModel: (_Os$cpus$ = _os.default.cpus()[0]) === null || _Os$cpus$ === void 0 ? void 0 : _Os$cpus$.model, cpuSpeed: (_Os$cpus$2 = _os.default.cpus()[0]) === null || _Os$cpus$2 === void 0 ? void 0 : _Os$cpus$2.speed, - email: isElasticCommitter ? email : undefined, freeMem: _os.default.freemem(), - isElasticCommitter, - kibanaUuid, + memoryUsageRss: memUsage.rss, + memoryUsageHeapTotal: memUsage.heapTotal, + memoryUsageHeapUsed: memUsage.heapUsed, + memoryUsageExternal: memUsage.external, + memoryUsageArrayBuffers: memUsage.arrayBuffers, nestedTiming: process.env.CI_STATS_NESTED_TIMING ? true : false, osArch: _os.default.arch(), osPlatform: _os.default.platform(), diff --git a/packages/kbn-securitysolution-es-utils/src/create_boostrap_index/index.ts b/packages/kbn-securitysolution-es-utils/src/create_boostrap_index/index.ts index 9671d35dc554..6a177f5caac2 100644 --- a/packages/kbn-securitysolution-es-utils/src/create_boostrap_index/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/create_boostrap_index/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; // See the reference(s) below on explanations about why -000001 was chosen and // why the is_write_index is true as well as the bootstrapping step which is needed. @@ -16,9 +16,8 @@ export const createBootstrapIndex = async ( index: string ): Promise => { return ( - await esClient.transport.request({ - path: `/${index}-000001`, - method: 'PUT', + await esClient.indices.create({ + index: `${index}-000001`, body: { aliases: { [index]: { diff --git a/packages/kbn-securitysolution-es-utils/src/delete_all_index/index.ts b/packages/kbn-securitysolution-es-utils/src/delete_all_index/index.ts index 4df4724aaf2b..580c104752ae 100644 --- a/packages/kbn-securitysolution-es-utils/src/delete_all_index/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/delete_all_index/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; export const deleteAllIndex = async ( esClient: ElasticsearchClient, diff --git a/packages/kbn-securitysolution-es-utils/src/delete_policy/index.ts b/packages/kbn-securitysolution-es-utils/src/delete_policy/index.ts index 34c1d2e5da45..60a15f6d4625 100644 --- a/packages/kbn-securitysolution-es-utils/src/delete_policy/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/delete_policy/index.ts @@ -6,16 +6,14 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; export const deletePolicy = async ( esClient: ElasticsearchClient, policy: string ): Promise => { return ( - await esClient.transport.request({ - path: `/_ilm/policy/${policy}`, - method: 'DELETE', - }) - ).body; + // @ts-expect-error policy_id is required by mistake. fixed in the v8.0 + (await esClient.ilm.deleteLifecycle({ policy })).body + ); }; diff --git a/packages/kbn-securitysolution-es-utils/src/delete_template/index.ts b/packages/kbn-securitysolution-es-utils/src/delete_template/index.ts index 2e7a71af9f77..86565a0c43b3 100644 --- a/packages/kbn-securitysolution-es-utils/src/delete_template/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/delete_template/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; export const deleteTemplate = async ( esClient: ElasticsearchClient, diff --git a/packages/kbn-securitysolution-es-utils/src/elasticsearch_client/index.ts b/packages/kbn-securitysolution-es-utils/src/elasticsearch_client/index.ts index 0c2252bdc1f0..a1fb3ff3ecf3 100644 --- a/packages/kbn-securitysolution-es-utils/src/elasticsearch_client/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/elasticsearch_client/index.ts @@ -10,12 +10,6 @@ // as these types aren't part of any package yet. Once they are, remove this completely import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; -import type { - ApiResponse, - TransportRequestOptions, - TransportRequestParams, - TransportRequestPromise, -} from '@elastic/elasticsearch/lib/Transport'; /** * Client used to query the elasticsearch cluster. @@ -25,11 +19,4 @@ import type { export type ElasticsearchClient = Omit< KibanaClient, 'connectionPool' | 'transport' | 'serializer' | 'extend' | 'child' | 'close' -> & { - transport: { - request( - params: TransportRequestParams, - options?: TransportRequestOptions - ): TransportRequestPromise; - }; -}; +>; diff --git a/packages/kbn-securitysolution-es-utils/src/get_index_aliases/index.ts b/packages/kbn-securitysolution-es-utils/src/get_index_aliases/index.ts index 885103c1fb58..ba00c1144edf 100644 --- a/packages/kbn-securitysolution-es-utils/src/get_index_aliases/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/get_index_aliases/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; interface AliasesResponse { [indexName: string]: { diff --git a/packages/kbn-securitysolution-es-utils/src/get_index_count/index.ts b/packages/kbn-securitysolution-es-utils/src/get_index_count/index.ts index 523b41303a56..b1dcd4fd0ad9 100644 --- a/packages/kbn-securitysolution-es-utils/src/get_index_count/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/get_index_count/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; /** * Retrieves the count of documents in a given index diff --git a/packages/kbn-securitysolution-es-utils/src/get_index_exists/index.ts b/packages/kbn-securitysolution-es-utils/src/get_index_exists/index.ts index b7d12cab3f48..920877304847 100644 --- a/packages/kbn-securitysolution-es-utils/src/get_index_exists/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/get_index_exists/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; export const getIndexExists = async ( esClient: ElasticsearchClient, diff --git a/packages/kbn-securitysolution-es-utils/src/get_policy_exists/index.ts b/packages/kbn-securitysolution-es-utils/src/get_policy_exists/index.ts index cefd47dbe9d0..8172cfb2abaa 100644 --- a/packages/kbn-securitysolution-es-utils/src/get_policy_exists/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/get_policy_exists/index.ts @@ -5,17 +5,15 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; export const getPolicyExists = async ( esClient: ElasticsearchClient, policy: string ): Promise => { try { - await esClient.transport.request({ - path: `/_ilm/policy/${policy}`, - method: 'GET', + await esClient.ilm.getLifecycle({ + policy, }); // Return true that there exists a policy which is not 404 or some error // Since there is not a policy exists API, this is how we create one by calling diff --git a/packages/kbn-securitysolution-es-utils/src/get_template_exists/index.ts b/packages/kbn-securitysolution-es-utils/src/get_template_exists/index.ts index c56c5b968d45..72a3a9365448 100644 --- a/packages/kbn-securitysolution-es-utils/src/get_template_exists/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/get_template_exists/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; export const getTemplateExists = async ( esClient: ElasticsearchClient, diff --git a/packages/kbn-securitysolution-es-utils/src/read_index/index.ts b/packages/kbn-securitysolution-es-utils/src/read_index/index.ts index cc16645120b7..206a4208b2ec 100644 --- a/packages/kbn-securitysolution-es-utils/src/read_index/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/read_index/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; export const readIndex = async (esClient: ElasticsearchClient, index: string): Promise => { return esClient.indices.get({ diff --git a/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts b/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts index aab641367339..772a6afa18c9 100644 --- a/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts @@ -6,16 +6,14 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; export const readPrivileges = async ( esClient: ElasticsearchClient, index: string ): Promise => { return ( - await esClient.transport.request({ - path: '/_security/user/_has_privileges', - method: 'POST', + await esClient.security.hasPrivileges({ body: { cluster: [ 'all', diff --git a/packages/kbn-securitysolution-es-utils/src/set_policy/index.ts b/packages/kbn-securitysolution-es-utils/src/set_policy/index.ts index dc45ca3e1c08..f6c2dcf7c3c3 100644 --- a/packages/kbn-securitysolution-es-utils/src/set_policy/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/set_policy/index.ts @@ -5,8 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; export const setPolicy = async ( esClient: ElasticsearchClient, @@ -14,9 +13,8 @@ export const setPolicy = async ( body: Record ): Promise => { return ( - await esClient.transport.request({ - path: `/_ilm/policy/${policy}`, - method: 'PUT', + await esClient.ilm.putLifecycle({ + policy, body, }) ).body; diff --git a/packages/kbn-securitysolution-es-utils/src/set_template/index.ts b/packages/kbn-securitysolution-es-utils/src/set_template/index.ts index 89aaa44f29e0..20f6fd5719d5 100644 --- a/packages/kbn-securitysolution-es-utils/src/set_template/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/set_template/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../elasticsearch_client'; +import type { ElasticsearchClient } from '../elasticsearch_client'; export const setTemplate = async ( esClient: ElasticsearchClient, diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts b/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts index b2cc8d6d8ffb..b2fd3de6bbbb 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts @@ -26,7 +26,8 @@ describe('createFailureIssue()', () => { time: '2018-01-01T01:00:00Z', likelyIrrelevant: false, }, - api + api, + 'main' ); expect(api.createIssue).toMatchInlineSnapshot(` @@ -40,7 +41,7 @@ describe('createFailureIssue()', () => { this is the failure text \`\`\` - First failure: [CI Build](https://build-url) + First failure: [CI Build - main](https://build-url) ", Array [ @@ -74,7 +75,8 @@ describe('updateFailureIssue()', () => { " `, }, - api + api, + 'main' ); expect(api.editIssueBodyAndEnsureOpen).toMatchInlineSnapshot(` @@ -100,7 +102,7 @@ describe('updateFailureIssue()', () => { "calls": Array [ Array [ 1234, - "New failure: [CI Build](https://build-url)", + "New failure: [CI Build - main](https://build-url)", ], ], "results": Array [ diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failure.ts b/packages/kbn-test/src/failed_tests_reporter/report_failure.ts index c881bc39abf6..c44fae560156 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failure.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failure.ts @@ -10,7 +10,12 @@ import { TestFailure } from './get_failures'; import { GithubIssueMini, GithubApi } from './github_api'; import { getIssueMetadata, updateIssueMetadata } from './issue_metadata'; -export async function createFailureIssue(buildUrl: string, failure: TestFailure, api: GithubApi) { +export async function createFailureIssue( + buildUrl: string, + failure: TestFailure, + api: GithubApi, + branch: string +) { const title = `Failing test: ${failure.classname} - ${failure.name}`; const body = updateIssueMetadata( @@ -21,7 +26,7 @@ export async function createFailureIssue(buildUrl: string, failure: TestFailure, failure.failure, '```', '', - `First failure: [CI Build](${buildUrl})`, + `First failure: [CI Build - ${branch}](${buildUrl})`, ].join('\n'), { 'test.class': failure.classname, @@ -33,7 +38,12 @@ export async function createFailureIssue(buildUrl: string, failure: TestFailure, return await api.createIssue(title, body, ['failed-test']); } -export async function updateFailureIssue(buildUrl: string, issue: GithubIssueMini, api: GithubApi) { +export async function updateFailureIssue( + buildUrl: string, + issue: GithubIssueMini, + api: GithubApi, + branch: string +) { // Increment failCount const newCount = getIssueMetadata(issue.body, 'test.failCount', 0) + 1; const newBody = updateIssueMetadata(issue.body, { @@ -41,7 +51,7 @@ export async function updateFailureIssue(buildUrl: string, issue: GithubIssueMin }); await api.editIssueBodyAndEnsureOpen(issue.number, newBody); - await api.addIssueComment(issue.number, `New failure: [CI Build](${buildUrl})`); + await api.addIssueComment(issue.number, `New failure: [CI Build - ${branch}](${buildUrl})`); return newCount; } diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts b/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts index aca2e6838fae..f075f6ef0b75 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts @@ -76,6 +76,12 @@ export function reportFailuresToFile(log: ToolingLog, failures: TestFailure[]) { .flat() .join('\n'); + // Buildkite steps that use `parallelism` need a numerical suffix added to identify them + // We should also increment the number by one, since it's 0-based + const jobNumberSuffix = process.env.BUILDKITE_PARALLEL_JOB + ? ` #${parseInt(process.env.BUILDKITE_PARALLEL_JOB, 10) + 1}` + : ''; + const failureJSON = JSON.stringify( { ...failure, @@ -84,9 +90,7 @@ export function reportFailuresToFile(log: ToolingLog, failures: TestFailure[]) { jobId: process.env.BUILDKITE_JOB_ID || '', url: process.env.BUILDKITE_BUILD_URL || '', jobName: process.env.BUILDKITE_LABEL - ? `${process.env.BUILDKITE_LABEL}${ - process.env.BUILDKITE_PARALLEL_JOB ? ` #${process.env.BUILDKITE_PARALLEL_JOB}` : '' - }` + ? `${process.env.BUILDKITE_LABEL}${jobNumberSuffix}` : '', }, null, diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index 6c88b7408b62..31cd43eae414 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -37,8 +37,8 @@ export function runFailedTestsReporterCli() { ); } + let branch: string = ''; if (updateGithub) { - let branch: string = ''; let isPr = false; if (process.env.BUILDKITE === 'true') { @@ -139,7 +139,12 @@ export function runFailedTestsReporterCli() { } if (existingIssue) { - const newFailureCount = await updateFailureIssue(buildUrl, existingIssue, githubApi); + const newFailureCount = await updateFailureIssue( + buildUrl, + existingIssue, + githubApi, + branch + ); const url = existingIssue.html_url; pushMessage(`Test has failed ${newFailureCount - 1} times on tracked branches: ${url}`); if (updateGithub) { @@ -148,7 +153,7 @@ export function runFailedTestsReporterCli() { continue; } - const newIssue = await createFailureIssue(buildUrl, failure, githubApi); + const newIssue = await createFailureIssue(buildUrl, failure, githubApi, branch); pushMessage('Test has not failed recently on tracked branches'); if (updateGithub) { pushMessage(`Created new issue: ${newIssue.html_url}`); diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index ccd578aa038f..3ad365a028b6 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -9,7 +9,7 @@ import { resolve } from 'path'; import { inspect } from 'util'; -import { run, createFlagError, Flags } from '@kbn/dev-utils'; +import { run, createFlagError, Flags, ToolingLog, getTimeReporter } from '@kbn/dev-utils'; import exitHook from 'exit-hook'; import { FunctionalTestRunner } from './functional_test_runner'; @@ -27,6 +27,12 @@ const parseInstallDir = (flags: Flags) => { }; export function runFtrCli() { + const runStartTime = Date.now(); + const toolingLog = new ToolingLog({ + level: 'info', + writeTo: process.stdout, + }); + const reportTime = getTimeReporter(toolingLog, 'scripts/functional_test_runner'); run( async ({ flags, log }) => { const functionalTestRunner = new FunctionalTestRunner( @@ -68,9 +74,19 @@ export function runFtrCli() { teardownRun = true; if (err) { + await reportTime(runStartTime, 'total', { + success: false, + err: err.message, + ...flags, + }); log.indent(-log.indent()); log.error(err); process.exitCode = 1; + } else { + await reportTime(runStartTime, 'total', { + success: true, + ...flags, + }); } try { diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js b/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js index 824cf3e6ceec..df7f8750b2ae 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js @@ -18,6 +18,8 @@ import { processOptions, displayHelp } from './args'; export async function startServersCli(defaultConfigPath) { await runCli(displayHelp, async (userOptions) => { const options = processOptions(userOptions, defaultConfigPath); - await startServers(options); + await startServers({ + ...options, + }); }); } diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index d45f8656ed72..3bc697c143f4 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -9,7 +9,7 @@ import { relative } from 'path'; import * as Rx from 'rxjs'; import { startWith, switchMap, take } from 'rxjs/operators'; -import { withProcRunner, ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { withProcRunner, ToolingLog, REPO_ROOT, getTimeReporter } from '@kbn/dev-utils'; import dedent from 'dedent'; import { @@ -147,7 +147,14 @@ interface StartServerOptions { useDefaultConfig?: boolean; } -export async function startServers(options: StartServerOptions) { +export async function startServers({ ...options }: StartServerOptions) { + const runStartTime = Date.now(); + const toolingLog = new ToolingLog({ + level: 'info', + writeTo: process.stdout, + }); + const reportTime = getTimeReporter(toolingLog, 'scripts/functional_tests_server'); + const log = options.createLogger(); const opts = { ...options, @@ -170,6 +177,11 @@ export async function startServers(options: StartServerOptions) { }, }); + reportTime(runStartTime, 'ready', { + success: true, + ...options, + }); + // wait for 5 seconds of silence before logging the // success message so that it doesn't get buried await silence(log, 5000); diff --git a/packages/kbn-test/src/jest/run.ts b/packages/kbn-test/src/jest/run.ts index 441104befde9..07610a3eb84c 100644 --- a/packages/kbn-test/src/jest/run.ts +++ b/packages/kbn-test/src/jest/run.ts @@ -21,7 +21,8 @@ import { resolve, relative, sep as osSep } from 'path'; import { existsSync } from 'fs'; import { run } from 'jest'; import { buildArgv } from 'jest-cli/build/cli'; -import { ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog, getTimeReporter } from '@kbn/dev-utils'; +import { map } from 'lodash'; // yarn test:jest src/core/server/saved_objects // yarn test:jest src/core/public/core_system.test.ts @@ -35,9 +36,14 @@ export function runJest(configName = 'jest.config.js') { writeTo: process.stdout, }); + const runStartTime = Date.now(); + const reportTime = getTimeReporter(log, 'scripts/jest'); + let cwd: string; + let testFiles: string[]; + if (!argv.config) { - const cwd = process.env.INIT_CWD || process.cwd(); - const testFiles = argv._.splice(2).map((p) => resolve(cwd, p)); + cwd = process.env.INIT_CWD || process.cwd(); + testFiles = argv._.splice(2).map((p) => resolve(cwd, p)); const commonTestFiles = commonBasePath(testFiles); const testFilesProvided = testFiles.length > 0; @@ -73,7 +79,14 @@ export function runJest(configName = 'jest.config.js') { process.env.NODE_ENV = 'test'; } - run(); + run().then(() => { + // Success means that tests finished, doesn't mean they passed. + reportTime(runStartTime, 'total', { + success: true, + isXpack: cwd.includes('x-pack'), + testFiles: map(testFiles, (testFile) => relative(cwd, testFile)), + }); + }); } /** diff --git a/packages/kbn-test/src/jest/utils/get_url.ts b/packages/kbn-test/src/jest/utils/get_url.ts index 734e26c5199d..e08695b334e1 100644 --- a/packages/kbn-test/src/jest/utils/get_url.ts +++ b/packages/kbn-test/src/jest/utils/get_url.ts @@ -22,11 +22,6 @@ interface UrlParam { username?: string; } -interface App { - pathname?: string; - hash?: string; -} - /** * Converts a config and a pathname to a url * @param {object} config A url config @@ -46,11 +41,11 @@ interface App { * @return {string} */ -function getUrl(config: UrlParam, app: App) { +function getUrl(config: UrlParam, app: UrlParam) { return url.format(_.assign({}, config, app)); } -getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: App) { +getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: UrlParam) { config = _.pickBy(config, function (val, param) { return param !== 'auth'; }); diff --git a/renovate.json5 b/renovate.json5 index 12a30876291d..dea7d311bae1 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -86,10 +86,9 @@ { groupName: 'platform security modules', packageNames: [ - 'broadcast-channel', - 'jsonwebtoken', '@types/jsonwebtoken', - 'node-forge', '@types/node-forge', - 'require-in-the-middle', + 'broadcast-channel', + 'node-forge', '@types/node-forge', + 'require-in-the-middle', 'tough-cookie', '@types/tough-cookie', 'xml-crypto', '@types/xml-crypto' ], diff --git a/src/cli/serve/integration_tests/__fixtures__/invalid_config.yml b/src/cli/serve/integration_tests/__fixtures__/invalid_config.yml index df9ea641cd3f..d8e59ced89c8 100644 --- a/src/cli/serve/integration_tests/__fixtures__/invalid_config.yml +++ b/src/cli/serve/integration_tests/__fixtures__/invalid_config.yml @@ -1,3 +1,13 @@ +logging: + root: + level: fatal + appenders: [console-json] + appenders: + console-json: + type: console + layout: + type: json + unknown: key: 1 diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml deleted file mode 100644 index 1761a7984e0e..000000000000 --- a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml +++ /dev/null @@ -1,13 +0,0 @@ -server: - autoListen: false - port: 8274 -logging: - json: true -optimize: - enabled: false -plugins: - initialize: false -migrations: - skip: true -elasticsearch: - skipStartupConnectionCheck: true diff --git a/src/cli/serve/integration_tests/invalid_config.test.ts b/src/cli/serve/integration_tests/invalid_config.test.ts index 724998699da8..2de902582a54 100644 --- a/src/cli/serve/integration_tests/invalid_config.test.ts +++ b/src/cli/serve/integration_tests/invalid_config.test.ts @@ -14,14 +14,15 @@ const INVALID_CONFIG_PATH = require.resolve('./__fixtures__/invalid_config.yml') interface LogEntry { message: string; - tags?: string[]; - type: string; + log: { + level: string; + }; } -describe('cli invalid config support', function () { +describe('cli invalid config support', () => { it( - 'exits with statusCode 64 and logs a single line when config is invalid', - function () { + 'exits with statusCode 64 and logs an error when config is invalid', + () => { // Unused keys only throw once LegacyService starts, so disable migrations so that Core // will finish the start lifecycle without a running Elasticsearch instance. const { error, status, stdout, stderr } = spawnSync( @@ -31,41 +32,27 @@ describe('cli invalid config support', function () { cwd: REPO_ROOT, } ); + expect(error).toBe(undefined); - let fatalLogLine; + let fatalLogEntries; try { - [fatalLogLine] = stdout + fatalLogEntries = stdout .toString('utf8') .split('\n') .filter(Boolean) .map((line) => JSON.parse(line) as LogEntry) - .filter((line) => line.tags?.includes('fatal')) - .map((obj) => ({ - ...obj, - pid: '## PID ##', - '@timestamp': '## @timestamp ##', - error: '## Error with stack trace ##', - })); + .filter((line) => line.log.level === 'FATAL'); } catch (e) { throw new Error( `error parsing log output:\n\n${e.stack}\n\nstdout: \n${stdout}\n\nstderr:\n${stderr}` ); } - expect(error).toBe(undefined); - - if (!fatalLogLine) { - throw new Error( - `cli did not log the expected fatal error message:\n\nstdout: \n${stdout}\n\nstderr:\n${stderr}` - ); - } - - expect(fatalLogLine.message).toContain( - 'Error: Unknown configuration key(s): "unknown.key", "other.unknown.key", "other.third", "some.flat.key", ' + + expect(fatalLogEntries).toHaveLength(1); + expect(fatalLogEntries[0].message).toContain( + 'Unknown configuration key(s): "unknown.key", "other.unknown.key", "other.third", "some.flat.key", ' + '"some.array". Check for spelling errors and ensure that expected plugins are installed.' ); - expect(fatalLogLine.tags).toEqual(['fatal', 'root']); - expect(fatalLogLine.type).toEqual('log'); expect(status).toBe(64); }, diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts index 80ce52661565..4cee7dfae412 100644 --- a/src/cli/serve/integration_tests/reload_logging_config.test.ts +++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts @@ -17,7 +17,6 @@ import { map, filter, take } from 'rxjs/operators'; import { safeDump } from 'js-yaml'; import { getConfigFromFiles } from '@kbn/config'; -const legacyConfig = follow('__fixtures__/reload_logging_config/kibana.test.yml'); const configFileLogConsole = follow( '__fixtures__/reload_logging_config/kibana_log_console.test.yml' ); @@ -96,81 +95,6 @@ describe.skip('Server logging configuration', function () { return; } - describe('legacy logging', () => { - it( - 'should be reloadable via SIGHUP process signaling', - async function () { - const configFilePath = Path.resolve(tempDir, 'kibana.yml'); - Fs.copyFileSync(legacyConfig, configFilePath); - - child = Child.spawn(process.execPath, [ - kibanaPath, - '--oss', - '--config', - configFilePath, - '--verbose', - ]); - - // TypeScript note: As long as the child stdio[1] is 'pipe', then stdout will not be null - const message$ = Rx.fromEvent(child.stdout!, 'data').pipe( - map((messages) => String(messages).split('\n').filter(Boolean)) - ); - - await message$ - .pipe( - // We know the sighup handler will be registered before this message logged - filter((messages: string[]) => messages.some((m) => m.includes('setting up root'))), - take(1) - ) - .toPromise(); - - const lastMessage = await message$.pipe(take(1)).toPromise(); - expect(containsJsonOnly(lastMessage)).toBe(true); - - createConfigManager(configFilePath).modify((oldConfig) => { - oldConfig.logging.json = false; - return oldConfig; - }); - - child.kill('SIGHUP'); - - await message$ - .pipe( - filter((messages) => !containsJsonOnly(messages)), - take(1) - ) - .toPromise(); - }, - minute - ); - - it( - 'should recreate file handle on SIGHUP', - async function () { - const logPath = Path.resolve(tempDir, 'kibana.log'); - const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); - - child = Child.spawn(process.execPath, [ - kibanaPath, - '--oss', - '--config', - legacyConfig, - '--logging.dest', - logPath, - '--verbose', - ]); - - await watchFileUntil(logPath, /setting up root/, 30 * second); - // once the server is running, archive the log file and issue SIGHUP - Fs.renameSync(logPath, logPathArchived); - child.kill('SIGHUP'); - - await watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 30 * second); - }, - minute - ); - }); - describe('platform logging', () => { it( 'should be reloadable via SIGHUP process signaling', diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 705acfe4fdf5..8b346d38cfea 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -124,17 +124,12 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { if (opts.elasticsearch) set('elasticsearch.hosts', opts.elasticsearch.split(',')); if (opts.port) set('server.port', opts.port); if (opts.host) set('server.host', opts.host); + if (opts.silent) { - set('logging.silent', true); set('logging.root.level', 'off'); } if (opts.verbose) { - if (has('logging.root.appenders')) { - set('logging.root.level', 'all'); - } else { - // Only set logging.verbose to true for legacy logging when KP logging isn't configured. - set('logging.verbose', true); - } + set('logging.root.level', 'all'); } set('plugins.paths', _.compact([].concat(get('plugins.paths'), opts.pluginPath))); @@ -159,9 +154,8 @@ export default function (program) { [getConfigPath()] ) .option('-p, --port ', 'The port to bind to', parseInt) - .option('-q, --quiet', 'Deprecated, set logging level in your configuration') - .option('-Q, --silent', 'Prevent all logging') - .option('--verbose', 'Turns on verbose logging') + .option('-Q, --silent', 'Set the root logger level to off') + .option('--verbose', 'Set the root logger level to all') .option('-H, --host ', 'The host to bind to') .option( '-l, --log-file ', @@ -217,8 +211,6 @@ export default function (program) { const cliArgs = { dev: !!opts.dev, envName: unknownOptions.env ? unknownOptions.env.name : undefined, - // no longer supported - quiet: !!opts.quiet, silent: !!opts.silent, verbose: !!opts.verbose, watch: !!opts.watch, diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 6e33e39b148c..6987b779d5d4 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -565,6 +565,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "data-test-subj": "homeLink", "href": "/", "iconType": "home", + "isActive": false, "label": "Home", "onClick": [Function], }, @@ -587,6 +588,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` data-test-subj="homeLink" href="/" iconType="home" + isActive={false} key="title-0" label="Home" onClick={[Function]} diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 1bf5c5e2f65c..ad590865b9e1 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -16,10 +16,11 @@ import { EuiListGroupItem, EuiShowFor, EuiCollapsibleNavProps, + EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { groupBy, sortBy } from 'lodash'; -import React, { Fragment, useRef } from 'react'; +import React, { Fragment, useMemo, useRef } from 'react'; import useObservable from 'react-use/lib/useObservable'; import * as Rx from 'rxjs'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..'; @@ -27,8 +28,12 @@ import { AppCategory } from '../../../../types'; import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; -import { createEuiListItem, createRecentNavLink, isModifiedOrPrevented } from './nav_link'; - +import { + createEuiListItem, + createRecentNavLink, + isModifiedOrPrevented, + createEuiButtonItem, +} from './nav_link'; function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; @@ -95,12 +100,28 @@ export function CollapsibleNav({ button, ...observables }: Props) { - const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); + const allLinks = useObservable(observables.navLinks$, []); + const allowedLinks = useMemo( + () => + allLinks.filter( + // Filterting out hidden links and the integrations one in favor of a specific Add Data button at the bottom + (link) => !link.hidden && link.id !== 'integrations' + ), + [allLinks] + ); + const integrationsLink = useMemo( + () => + allLinks.find( + // Find just the integrations link + (link) => link.id === 'integrations' + ), + [allLinks] + ); const recentlyAccessed = useObservable(observables.recentlyAccessed$, []); const customNavLink = useObservable(observables.customNavLink$, undefined); const appId = useObservable(observables.appId$, ''); const lockRef = useRef(null); - const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id); + const groupedNavLinks = groupBy(allowedLinks, (link) => link?.category?.id); const { undefined: unknowns = [], ...allCategorizedLinks } = groupedNavLinks; const categoryDictionary = getAllCategories(allCategorizedLinks); const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary); @@ -176,6 +197,7 @@ export function CollapsibleNav({ iconType: 'home', href: homeHref, 'data-test-subj': 'homeLink', + isActive: appId === 'home', onClick: (event) => { if (isModifiedOrPrevented(event)) { return; @@ -217,7 +239,7 @@ export function CollapsibleNav({ // Can remove icon from recent links completely const { iconType, onClick, ...hydratedLink } = createRecentNavLink( link, - navLinks, + allowedLinks, basePath, navigateToUrl ); @@ -323,6 +345,29 @@ export function CollapsibleNav({ + {integrationsLink && ( + + {/* Span fakes the nav group into not being the first item and therefore adding a top border */} + + + + {i18n.translate('core.ui.primaryNav.addData', { + defaultMessage: 'Add data', + })} + + + + )} ); } diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index b0ebf7cc5f8e..b6d8be8a3c78 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -14,7 +14,7 @@ import { HttpStart } from '../../../http'; import { InternalApplicationStart } from '../../../application/types'; import { relativeToAbsolute } from '../../nav_links/to_nav_link'; -export const isModifiedOrPrevented = (event: React.MouseEvent) => +export const isModifiedOrPrevented = (event: React.MouseEvent) => event.metaKey || event.altKey || event.ctrlKey || event.shiftKey || event.defaultPrevented; interface Props { @@ -71,6 +71,29 @@ export function createEuiListItem({ }; } +export function createEuiButtonItem({ + link, + onClick = () => {}, + navigateToUrl, + dataTestSubj, +}: Omit) { + const { href, disabled, url } = link; + + return { + href, + /* Use href and onClick to support "open in new tab" and SPA navigation in the same link */ + onClick(event: React.MouseEvent) { + if (!isModifiedOrPrevented(event)) { + onClick(); + } + event.preventDefault(); + navigateToUrl(url); + }, + isDisabled: disabled, + 'data-test-subj': dataTestSubj, + }; +} + export interface RecentNavLink { href: string; label: string; diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts index 101f86b299ff..3fc2f0fbb317 100644 --- a/src/core/public/saved_objects/saved_objects_client.test.ts +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -216,61 +216,51 @@ describe('SavedObjectsClient', () => { `); }); - test('removes duplicates when calling `_bulk_resolve`', async () => { + test('handles duplicates correctly', async () => { // Await #resolve call to ensure batchQueue is empty and throttle has reset mockResolvedObjects({ ...doc, type: 'type2' }); await savedObjectsClient.resolve('type2', doc.id); http.fetch.mockClear(); - mockResolvedObjects(doc, { ...doc, type: 'some-type', id: 'some-id' }); // the client will only request two objects, so we only mock two results - savedObjectsClient.resolve(doc.type, doc.id); - savedObjectsClient.resolve('some-type', 'some-id'); - await savedObjectsClient.resolve(doc.type, doc.id); + mockResolvedObjects(doc, { ...doc, type: 'type2' }, { ...doc, type: 'type3' }); // the client will only request three objects, so we only mock three results + const call1 = savedObjectsClient.resolve(doc.type, doc.id); + const call2 = savedObjectsClient.resolve('type2', doc.id); + const call3 = savedObjectsClient.resolve(doc.type, doc.id); + const objFromCall4 = await savedObjectsClient.resolve('type3', doc.id); + const objFromCall1 = await call1; + const objFromCall2 = await call2; + const objFromCall3 = await call3; + // Assertion 1: all calls should return the expected object + expect(objFromCall1.saved_object).toEqual( + expect.objectContaining({ type: doc.type, id: doc.id, error: undefined }) + ); + expect(objFromCall2.saved_object).toEqual( + expect.objectContaining({ type: 'type2', id: doc.id, error: undefined }) + ); + expect(objFromCall3.saved_object).toEqual( + expect.objectContaining({ type: doc.type, id: doc.id, error: undefined }) + ); + expect(objFromCall4.saved_object).toEqual( + expect.objectContaining({ type: 'type3', id: doc.id, error: undefined }) + ); + + // Assertion 2: requests should be deduplicated (call1 and call3) expect(http.fetch).toHaveBeenCalledTimes(1); expect(http.fetch.mock.calls[0]).toMatchInlineSnapshot(` Array [ "/api/saved_objects/_bulk_resolve", Object { - "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\"},{\\"id\\":\\"some-id\\",\\"type\\":\\"some-type\\"}]", + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\"},{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"type2\\"},{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"type3\\"}]", "method": "POST", "query": undefined, }, ] `); - }); - - test('resolves with correct object when there are duplicates present', async () => { - // Await #resolve call to ensure batchQueue is empty and throttle has reset - mockResolvedObjects({ ...doc, type: 'type2' }); - await savedObjectsClient.resolve('type2', doc.id); - http.fetch.mockClear(); - - mockResolvedObjects(doc); - const call1 = savedObjectsClient.resolve(doc.type, doc.id); - const objFromCall2 = await savedObjectsClient.resolve(doc.type, doc.id); - const objFromCall1 = await call1; - - expect(objFromCall1.saved_object.type).toBe(doc.type); - expect(objFromCall1.saved_object.id).toBe(doc.id); - - expect(objFromCall2.saved_object.type).toBe(doc.type); - expect(objFromCall2.saved_object.id).toBe(doc.id); - }); - - test('do not share instances or references between duplicate callers', async () => { - // Await #resolve call to ensure batchQueue is empty and throttle has reset - await savedObjectsClient.resolve('type2', doc.id); - mockResolvedObjects({ ...doc, type: 'type2' }); - http.fetch.mockClear(); - - mockResolvedObjects(doc); - const call1 = savedObjectsClient.resolve(doc.type, doc.id); - const objFromCall2 = await savedObjectsClient.resolve(doc.type, doc.id); - const objFromCall1 = await call1; + // Assertion 3: deduplicated requests should not share response object instances or references objFromCall1.saved_object.set('title', 'new title'); - expect(objFromCall2.saved_object.get('title')).toEqual('Example title'); + expect(objFromCall3.saved_object.get('title')).toEqual('Example title'); // unchanged }); test('resolves with ResolvedSimpleSavedObject instance', async () => { diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 0b0bc58729e3..d3810d8932f1 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -167,13 +167,13 @@ const getObjectsToResolve = (queue: BatchResolveQueueEntry[]) => { const responseIndices: number[] = []; const objectsToResolve: ObjectTypeAndId[] = []; const inserted = new Map(); - queue.forEach(({ id, type }, currentIndex) => { + queue.forEach(({ id, type }) => { const key = `${type}|${id}`; const indexForTypeAndId = inserted.get(key); if (indexForTypeAndId === undefined) { - inserted.set(key, currentIndex); + inserted.set(key, objectsToResolve.length); + responseIndices.push(objectsToResolve.length); objectsToResolve.push({ id, type }); - responseIndices.push(currentIndex); } else { responseIndices.push(indexForTypeAndId); } diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index d3a4d7f99706..4e99f46ea05f 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -8,6 +8,7 @@ import { getDeprecationsForGlobalSettings } from '../test_utils'; import { coreDeprecationProvider } from './core_deprecations'; + const initialEnv = { ...process.env }; const applyCoreDeprecations = (settings?: Record) => @@ -203,230 +204,4 @@ describe('core deprecations', () => { ).toEqual([`worker-src blob:`]); }); }); - - describe('logging.events.ops', () => { - it('warns when ops events are used', () => { - const { messages } = applyCoreDeprecations({ - logging: { events: { ops: '*' } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.events.ops\\" has been deprecated and will be removed in 8.0. To access ops data moving forward, please enable debug logs for the \\"metrics.ops\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", - ] - `); - }); - }); - - describe('logging.events.request and logging.events.response', () => { - it('warns when request and response events are used', () => { - const { messages } = applyCoreDeprecations({ - logging: { events: { request: '*', response: '*' } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", - ] - `); - }); - - it('warns when only request event is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { events: { request: '*' } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", - ] - `); - }); - - it('warns when only response event is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { events: { response: '*' } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", - ] - `); - }); - }); - - describe('logging.timezone', () => { - it('warns when ops events are used', () => { - const { messages } = applyCoreDeprecations({ - logging: { timezone: 'GMT' }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.timezone\\" has been deprecated and will be removed in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", - ] - `); - }); - }); - - describe('logging.dest', () => { - it('warns when dest is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { dest: 'stdout' }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", - ] - `); - }); - it('warns when dest path is given', () => { - const { messages } = applyCoreDeprecations({ - logging: { dest: '/log-log.txt' }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", - ] - `); - }); - }); - - describe('logging.quiet, logging.silent and logging.verbose', () => { - it('warns when quiet is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { quiet: true }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.quiet\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:error\\" in your logging configuration. ", - ] - `); - }); - it('warns when silent is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { silent: true }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.silent\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:off\\" in your logging configuration. ", - ] - `); - }); - it('warns when verbose is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { verbose: true }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.verbose\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:all\\" in your logging configuration. ", - ] - `); - }); - }); - - describe('logging.json', () => { - it('warns when json is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { json: true }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.json\\" has been deprecated and will be removed in 8.0. To specify log message format moving forward, you can configure the \\"appender.layout\\" property for every custom appender in your logging configuration. There is currently no default layout for custom appenders and each one must be declared explicitly. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", - ] - `); - }); - }); - - describe('logging.rotate.enabled, logging.rotate.usePolling, logging.rotate.pollingInterval, logging.rotate.everyBytes and logging.rotate.keepFiles', () => { - it('warns when logging.rotate configurations are used', () => { - const { messages } = applyCoreDeprecations({ - logging: { rotate: { enabled: true } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender", - ] - `); - }); - - it('warns when logging.rotate polling configurations are used', () => { - const { messages } = applyCoreDeprecations({ - logging: { rotate: { enabled: true, usePolling: true, pollingInterval: 5000 } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender", - ] - `); - }); - - it('warns when logging.rotate.everyBytes configurations are used', () => { - const { messages } = applyCoreDeprecations({ - logging: { rotate: { enabled: true, everyBytes: 1048576 } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender", - ] - `); - }); - - it('warns when logging.rotate.keepFiles is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { rotate: { enabled: true, keepFiles: 1024 } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender", - ] - `); - }); - }); - - describe('logging.events.log', () => { - it('warns when events.log is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { events: { log: ['info'] } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.events.log\\" has been deprecated and will be removed in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration.", - ] - `); - }); - }); - - describe('logging.events.error', () => { - it('warns when events.error is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { events: { error: ['some error'] } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.events.error\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level: error\\" in your logging configuration.", - ] - `); - }); - }); - - describe('logging.filter', () => { - it('warns when filter.cookie is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { filter: { cookie: 'none' } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.filter\\" has been deprecated and will be removed in 8.0.", - ] - `); - }); - - it('warns when filter.authorization is used', () => { - const { messages } = applyCoreDeprecations({ - logging: { filter: { authorization: 'none' } }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"logging.filter\\" has been deprecated and will be removed in 8.0.", - ] - `); - }); - }); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 6e7365d0d5cb..674812bd0957 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -113,245 +113,6 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati } }; -const opsLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (settings.logging?.events?.ops) { - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', - message: - '"logging.events.ops" has been deprecated and will be removed ' + - 'in 8.0. To access ops data moving forward, please enable debug logs for the ' + - '"metrics.ops" context in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', - correctiveActions: { - manualSteps: [ - `Remove "logging.events.ops" from your kibana settings.`, - `Enable debug logs for the "metrics.ops" context in your logging configuration`, - ], - }, - }); - } -}; - -const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (settings.logging?.events?.request || settings.logging?.events?.response) { - const removeConfigsSteps = []; - - if (settings.logging?.events?.request) { - removeConfigsSteps.push(`Remove "logging.events.request" from your kibana configs.`); - } - - if (settings.logging?.events?.response) { - removeConfigsSteps.push(`Remove "logging.events.response" from your kibana configs.`); - } - - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', - message: - '"logging.events.request" and "logging.events.response" have been deprecated and will be removed ' + - 'in 8.0. To access request and/or response data moving forward, please enable debug logs for the ' + - '"http.server.response" context in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', - correctiveActions: { - manualSteps: [ - ...removeConfigsSteps, - `enable debug logs for the "http.server.response" context in your logging configuration.`, - ], - }, - }); - } -}; - -const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (settings.logging?.timezone) { - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingtimezone', - message: - '"logging.timezone" has been deprecated and will be removed ' + - 'in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern ' + - 'in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', - correctiveActions: { - manualSteps: [ - `Remove "logging.timezone" from your kibana configs.`, - `To set the timezone add a timezone date modifier to the log pattern in your logging configuration.`, - ], - }, - }); - } -}; - -const destLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (settings.logging?.dest) { - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingdest', - message: - '"logging.dest" has been deprecated and will be removed ' + - 'in 8.0. To set the destination moving forward, you can use the "console" appender ' + - 'in your logging configuration or define a custom one. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', - correctiveActions: { - manualSteps: [ - `Remove "logging.dest" from your kibana configs.`, - `To set the destination use the "console" appender in your logging configuration or define a custom one.`, - ], - }, - }); - } -}; - -const quietLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (settings.logging?.quiet) { - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingquiet', - message: - '"logging.quiet" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level:error" in your logging configuration. ', - correctiveActions: { - manualSteps: [ - `Remove "logging.quiet" from your kibana configs.`, - `Use "logging.root.level:error" in your logging configuration.`, - ], - }, - }); - } -}; - -const silentLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (settings.logging?.silent) { - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingsilent', - message: - '"logging.silent" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level:off" in your logging configuration. ', - correctiveActions: { - manualSteps: [ - `Remove "logging.silent" from your kibana configs.`, - `Use "logging.root.level:off" in your logging configuration.`, - ], - }, - }); - } -}; - -const verboseLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (settings.logging?.verbose) { - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingverbose', - message: - '"logging.verbose" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level:all" in your logging configuration. ', - correctiveActions: { - manualSteps: [ - `Remove "logging.verbose" from your kibana configs.`, - `Use "logging.root.level:all" in your logging configuration.`, - ], - }, - }); - } -}; - -const jsonLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - // We silence the deprecation warning when running in development mode because - // the dev CLI code in src/dev/cli_dev_mode/using_server_process.ts manually - // specifies `--logging.json=false`. Since it's executed in a child process, the - // ` legacyLoggingConfigSchema` returns `true` for the TTY check on `process.stdout.isTTY` - if (settings.logging?.json && settings.env !== 'development') { - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', - message: - '"logging.json" has been deprecated and will be removed ' + - 'in 8.0. To specify log message format moving forward, ' + - 'you can configure the "appender.layout" property for every custom appender in your logging configuration. ' + - 'There is currently no default layout for custom appenders and each one must be declared explicitly. ' + - 'For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', - correctiveActions: { - manualSteps: [ - `Remove "logging.json" from your kibana configs.`, - `Configure the "appender.layout" property for every custom appender in your logging configuration.`, - ], - }, - }); - } -}; - -const logRotateDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (settings.logging?.rotate) { - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender', - message: - '"logging.rotate" and sub-options have been deprecated and will be removed in 8.0. ' + - 'Moving forward, you can enable log rotation using the "rolling-file" appender for a logger ' + - 'in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender', - correctiveActions: { - manualSteps: [ - `Remove "logging.rotate" from your kibana configs.`, - `Enable log rotation using the "rolling-file" appender for a logger in your logging configuration.`, - ], - }, - }); - } -}; - -const logEventsLogDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (settings.logging?.events?.log) { - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', - message: - '"logging.events.log" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration.', - correctiveActions: { - manualSteps: [ - `Remove "logging.events.log" from your kibana configs.`, - `Customize log levels can be per-logger using the new logging configuration.`, - ], - }, - }); - } -}; - -const logEventsErrorDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (settings.logging?.events?.error) { - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', - message: - '"logging.events.error" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level: error" in your logging configuration.', - correctiveActions: { - manualSteps: [ - `Remove "logging.events.error" from your kibana configs.`, - `Use "logging.root.level: error" in your logging configuration.`, - ], - }, - }); - } -}; - -const logFilterDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (settings.logging?.filter) { - addDeprecation({ - documentationUrl: - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingfilter', - message: '"logging.filter" has been deprecated and will be removed in 8.0.', - correctiveActions: { - manualSteps: [`Remove "logging.filter" from your kibana configs.`], - }, - }); - } -}; - export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [ rename('cpu.cgroup.path.override', 'ops.cGroupOverrides.cpuPath'), rename('cpuacct.cgroup.path.override', 'ops.cGroupOverrides.cpuAcctPath'), @@ -360,16 +121,4 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unu kibanaPathConf, rewriteBasePathDeprecation, cspRulesDeprecation, - opsLoggingEventDeprecation, - requestLoggingEventDeprecation, - timezoneLoggingDeprecation, - destLoggingDeprecation, - quietLoggingDeprecation, - silentLoggingDeprecation, - verboseLoggingDeprecation, - jsonLoggingDeprecation, - logRotateDeprecation, - logEventsLogDeprecation, - logEventsErrorDeprecation, - logFilterDeprecation, ]; diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index 686564c6d678..7254dd5222b8 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -30,5 +30,4 @@ export type { ConfigDeprecationFactory, EnvironmentMode, PackageInfo, - LegacyObjectToConfigAdapter, } from '@kbn/config'; diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts index 0138c6e7ef15..5036fa4742b5 100644 --- a/src/core/server/config/integration_tests/config_deprecation.test.ts +++ b/src/core/server/config/integration_tests/config_deprecation.test.ts @@ -23,17 +23,13 @@ describe('configuration deprecations', () => { } }); - it('should not log deprecation warnings for default configuration that is not one of `logging.verbose`, `logging.quiet` or `logging.silent`', async () => { + it('should not log deprecation warnings for default configuration', async () => { root = kbnTestServer.createRoot(); await root.preboot(); await root.setup(); const logs = loggingSystemMock.collect(mockLoggingSystem); - expect(logs.warn.flat()).toMatchInlineSnapshot(` - Array [ - "\\"logging.silent\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:off\\" in your logging configuration. ", - ] - `); + expect(logs.warn.flat()).toHaveLength(0); }); }); diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 995b3ffbd947..7470ff708171 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -211,7 +211,7 @@ const deprecations: ConfigDeprecationProvider = () => [ }); } else if (es.logQueries === true) { addDeprecation({ - message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers" or use "logging.verbose: true".`, + message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers".`, correctiveActions: { manualSteps: [ `Remove Setting [${fromPath}.logQueries] from your kibana configs`, diff --git a/src/core/server/http/integration_tests/logging.test.ts b/src/core/server/http/integration_tests/logging.test.ts index 12d555a240cd..20e0175d4b19 100644 --- a/src/core/server/http/integration_tests/logging.test.ts +++ b/src/core/server/http/integration_tests/logging.test.ts @@ -51,7 +51,6 @@ describe('request logging', () => { it('logs at the correct level and with the correct context', async () => { const root = kbnTestServer.createRoot({ logging: { - silent: true, appenders: { 'test-console': { type: 'console', @@ -99,7 +98,6 @@ describe('request logging', () => { let root: ReturnType; const config = { logging: { - silent: true, appenders: { 'test-console': { type: 'console', @@ -300,7 +298,6 @@ describe('request logging', () => { it('filters sensitive request headers when RewriteAppender is configured', async () => { root = kbnTestServer.createRoot({ logging: { - silent: true, appenders: { 'test-console': { type: 'console', @@ -402,7 +399,6 @@ describe('request logging', () => { it('filters sensitive response headers when RewriteAppender is configured', async () => { root = kbnTestServer.createRoot({ logging: { - silent: true, appenders: { 'test-console': { type: 'console', diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts deleted file mode 100644 index a79e434ce457..000000000000 --- a/src/core/server/legacy/integration_tests/logging.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { LegacyLoggingConfig } from '@kbn/config'; -import * as kbnTestServer from '../../../test_helpers/kbn_server'; - -import { - getPlatformLogsFromMock, - getLegacyPlatformLogsFromMock, -} from '../../logging/integration_tests/utils'; - -function createRoot(legacyLoggingConfig: LegacyLoggingConfig = {}) { - return kbnTestServer.createRoot({ - migrations: { skip: true }, // otherwise stuck in polling ES - plugins: { initialize: false }, - elasticsearch: { skipStartupConnectionCheck: true }, - logging: { - // legacy platform config - silent: false, - json: false, - ...legacyLoggingConfig, - events: { - log: ['test-file-legacy'], - }, - // platform config - appenders: { - 'test-console': { - type: 'console', - layout: { - highlight: false, - type: 'pattern', - }, - }, - }, - loggers: [ - { - name: 'test-file', - appenders: ['test-console'], - level: 'info', - }, - ], - }, - }); -} - -describe('logging service', () => { - let mockConsoleLog: jest.SpyInstance; - let mockStdout: jest.SpyInstance; - - beforeAll(async () => { - mockConsoleLog = jest.spyOn(global.console, 'log'); - mockStdout = jest.spyOn(global.process.stdout, 'write'); - }); - - afterAll(async () => { - mockConsoleLog.mockRestore(); - mockStdout.mockRestore(); - }); - - describe('compatibility', () => { - describe('uses configured loggers', () => { - let root: ReturnType; - beforeAll(async () => { - root = createRoot(); - - await root.preboot(); - await root.setup(); - await root.start(); - }, 30000); - - afterAll(async () => { - await root.shutdown(); - }); - - beforeEach(() => { - mockConsoleLog.mockClear(); - mockStdout.mockClear(); - }); - - it('when context matches', async () => { - root.logger.get('test-file').info('handled by NP'); - - expect(mockConsoleLog).toHaveBeenCalledTimes(1); - const loggedString = getPlatformLogsFromMock(mockConsoleLog); - expect(loggedString).toMatchInlineSnapshot(` - Array [ - "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] handled by NP", - ] - `); - }); - - it('falls back to the root legacy logger otherwise', async () => { - root.logger.get('test-file-legacy').info('handled by LP'); - - expect(mockStdout).toHaveBeenCalledTimes(1); - - const loggedString = getLegacyPlatformLogsFromMock(mockStdout); - expect(loggedString).toMatchInlineSnapshot(` - Array [ - " log [xx:xx:xx.xxx] [info][test-file-legacy] handled by LP - ", - ] - `); - }); - }); - - describe('logging config respects legacy logging settings', () => { - let root: ReturnType; - - afterEach(async () => { - mockConsoleLog.mockClear(); - mockStdout.mockClear(); - await root.shutdown(); - }); - - it('"silent": true', async () => { - root = createRoot({ silent: true }); - - await root.preboot(); - await root.setup(); - await root.start(); - - const platformLogger = root.logger.get('test-file'); - platformLogger.info('info'); - platformLogger.warn('warn'); - platformLogger.error('error'); - - expect(mockConsoleLog).toHaveBeenCalledTimes(3); - - expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` - Array [ - "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info", - "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn", - "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error", - ] - `); - - mockStdout.mockClear(); - - const legacyPlatformLogger = root.logger.get('test-file-legacy'); - legacyPlatformLogger.info('info'); - legacyPlatformLogger.warn('warn'); - legacyPlatformLogger.error('error'); - - expect(mockStdout).toHaveBeenCalledTimes(0); - }); - - it('"quiet": true', async () => { - root = createRoot({ quiet: true }); - - await root.preboot(); - await root.setup(); - await root.start(); - - const platformLogger = root.logger.get('test-file'); - platformLogger.info('info'); - platformLogger.warn('warn'); - platformLogger.error('error'); - - expect(mockConsoleLog).toHaveBeenCalledTimes(3); - - expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` - Array [ - "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info", - "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn", - "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error", - ] - `); - - mockStdout.mockClear(); - - const legacyPlatformLogger = root.logger.get('test-file-legacy'); - legacyPlatformLogger.info('info'); - legacyPlatformLogger.warn('warn'); - legacyPlatformLogger.error('error'); - - expect(mockStdout).toHaveBeenCalledTimes(1); - expect(getLegacyPlatformLogsFromMock(mockStdout)).toMatchInlineSnapshot(` - Array [ - " log [xx:xx:xx.xxx] [error][test-file-legacy] error - ", - ] - `); - }); - - it('"verbose": true', async () => { - root = createRoot({ verbose: true }); - - await root.preboot(); - await root.setup(); - await root.start(); - - const platformLogger = root.logger.get('test-file'); - platformLogger.info('info'); - platformLogger.warn('warn'); - platformLogger.error('error'); - - expect(mockConsoleLog).toHaveBeenCalledTimes(3); - - expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` - Array [ - "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info", - "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn", - "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error", - ] - `); - - mockStdout.mockClear(); - - const legacyPlatformLogger = root.logger.get('test-file-legacy'); - legacyPlatformLogger.info('info'); - legacyPlatformLogger.warn('warn'); - legacyPlatformLogger.error('error'); - - expect(mockStdout).toHaveBeenCalledTimes(3); - expect(getLegacyPlatformLogsFromMock(mockStdout)).toMatchInlineSnapshot(` - Array [ - " log [xx:xx:xx.xxx] [info][test-file-legacy] info - ", - " log [xx:xx:xx.xxx] [warning][test-file-legacy] warn - ", - " log [xx:xx:xx.xxx] [error][test-file-legacy] error - ", - ] - `); - }); - }); - }); -}); diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts deleted file mode 100644 index 0d72318a630e..000000000000 --- a/src/core/server/legacy/legacy_service.mock.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { LegacyService } from './legacy_service'; - -type LegacyServiceMock = jest.Mocked>; - -const createLegacyServiceMock = (): LegacyServiceMock => ({ - setup: jest.fn(), - stop: jest.fn(), -}); - -export const legacyServiceMock = { - create: createLegacyServiceMock, -}; diff --git a/src/core/server/legacy/legacy_service.test.mocks.ts b/src/core/server/legacy/legacy_service.test.mocks.ts deleted file mode 100644 index 506f0fd6f96d..000000000000 --- a/src/core/server/legacy/legacy_service.test.mocks.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export const reconfigureLoggingMock = jest.fn(); -export const setupLoggingMock = jest.fn(); -export const setupLoggingRotateMock = jest.fn(); - -jest.doMock('@kbn/legacy-logging', () => ({ - ...(jest.requireActual('@kbn/legacy-logging') as any), - reconfigureLogging: reconfigureLoggingMock, - setupLogging: setupLoggingMock, - setupLoggingRotate: setupLoggingRotateMock, -})); diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts deleted file mode 100644 index 6b20bd7434ba..000000000000 --- a/src/core/server/legacy/legacy_service.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - setupLoggingMock, - setupLoggingRotateMock, - reconfigureLoggingMock, -} from './legacy_service.test.mocks'; - -import { BehaviorSubject } from 'rxjs'; -import moment from 'moment'; -import { REPO_ROOT } from '@kbn/dev-utils'; - -import { Config, Env, ObjectToConfigAdapter } from '../config'; - -import { getEnvOptions, configServiceMock } from '../config/mocks'; -import { loggingSystemMock } from '../logging/logging_system.mock'; -import { httpServiceMock } from '../http/http_service.mock'; -import { LegacyService, LegacyServiceSetupDeps } from './legacy_service'; - -let coreId: symbol; -let env: Env; -let config$: BehaviorSubject; - -let setupDeps: LegacyServiceSetupDeps; - -const logger = loggingSystemMock.create(); -let configService: ReturnType; - -beforeEach(() => { - coreId = Symbol(); - env = Env.createDefault(REPO_ROOT, getEnvOptions()); - configService = configServiceMock.create(); - - setupDeps = { - http: httpServiceMock.createInternalSetupContract(), - }; - - config$ = new BehaviorSubject( - new ObjectToConfigAdapter({ - elasticsearch: { hosts: ['http://127.0.0.1'] }, - server: { autoListen: true }, - }) - ); - - configService.getConfig$.mockReturnValue(config$); -}); - -afterEach(() => { - jest.clearAllMocks(); - setupLoggingMock.mockReset(); - setupLoggingRotateMock.mockReset(); - reconfigureLoggingMock.mockReset(); -}); - -describe('#setup', () => { - it('initializes legacy logging', async () => { - const opsConfig = { - interval: moment.duration(5, 'second'), - }; - const opsConfig$ = new BehaviorSubject(opsConfig); - - const loggingConfig = { - foo: 'bar', - }; - const loggingConfig$ = new BehaviorSubject(loggingConfig); - - configService.atPath.mockImplementation((path) => { - if (path === 'ops') { - return opsConfig$; - } - if (path === 'logging') { - return loggingConfig$; - } - return new BehaviorSubject({}); - }); - - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, - }); - - await legacyService.setup(setupDeps); - - expect(setupLoggingMock).toHaveBeenCalledTimes(1); - expect(setupLoggingMock).toHaveBeenCalledWith( - setupDeps.http.server, - loggingConfig, - opsConfig.interval.asMilliseconds() - ); - - expect(setupLoggingRotateMock).toHaveBeenCalledTimes(1); - expect(setupLoggingRotateMock).toHaveBeenCalledWith(setupDeps.http.server, loggingConfig); - }); - - it('reloads the logging config when the config changes', async () => { - const opsConfig = { - interval: moment.duration(5, 'second'), - }; - const opsConfig$ = new BehaviorSubject(opsConfig); - - const loggingConfig = { - foo: 'bar', - }; - const loggingConfig$ = new BehaviorSubject(loggingConfig); - - configService.atPath.mockImplementation((path) => { - if (path === 'ops') { - return opsConfig$; - } - if (path === 'logging') { - return loggingConfig$; - } - return new BehaviorSubject({}); - }); - - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, - }); - - await legacyService.setup(setupDeps); - - expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); - expect(reconfigureLoggingMock).toHaveBeenCalledWith( - setupDeps.http.server, - loggingConfig, - opsConfig.interval.asMilliseconds() - ); - - loggingConfig$.next({ - foo: 'changed', - }); - - expect(reconfigureLoggingMock).toHaveBeenCalledTimes(2); - expect(reconfigureLoggingMock).toHaveBeenCalledWith( - setupDeps.http.server, - { foo: 'changed' }, - opsConfig.interval.asMilliseconds() - ); - }); - - it('stops reloading logging config once the service is stopped', async () => { - const opsConfig = { - interval: moment.duration(5, 'second'), - }; - const opsConfig$ = new BehaviorSubject(opsConfig); - - const loggingConfig = { - foo: 'bar', - }; - const loggingConfig$ = new BehaviorSubject(loggingConfig); - - configService.atPath.mockImplementation((path) => { - if (path === 'ops') { - return opsConfig$; - } - if (path === 'logging') { - return loggingConfig$; - } - return new BehaviorSubject({}); - }); - - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, - }); - - await legacyService.setup(setupDeps); - - expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); - expect(reconfigureLoggingMock).toHaveBeenCalledWith( - setupDeps.http.server, - loggingConfig, - opsConfig.interval.asMilliseconds() - ); - - await legacyService.stop(); - - loggingConfig$.next({ - foo: 'changed', - }); - - expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts deleted file mode 100644 index 1d5343ff5311..000000000000 --- a/src/core/server/legacy/legacy_service.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { combineLatest, Observable, Subscription } from 'rxjs'; -import { first } from 'rxjs/operators'; -import { Server } from '@hapi/hapi'; -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { - reconfigureLogging, - setupLogging, - setupLoggingRotate, - LegacyLoggingConfig, -} from '@kbn/legacy-logging'; - -import { CoreContext } from '../core_context'; -import { config as loggingConfig } from '../logging'; -import { opsConfig, OpsConfigType } from '../metrics'; -import { Logger } from '../logging'; -import { InternalHttpServiceSetup } from '../http'; - -export interface LegacyServiceSetupDeps { - http: InternalHttpServiceSetup; -} - -/** @internal */ -export type ILegacyService = PublicMethodsOf; - -/** @internal */ -export class LegacyService { - private readonly log: Logger; - private readonly opsConfig$: Observable; - private readonly legacyLoggingConfig$: Observable; - private configSubscription?: Subscription; - - constructor(coreContext: CoreContext) { - const { logger, configService } = coreContext; - - this.log = logger.get('legacy-service'); - this.legacyLoggingConfig$ = configService.atPath(loggingConfig.path); - this.opsConfig$ = configService.atPath(opsConfig.path); - } - - public async setup(setupDeps: LegacyServiceSetupDeps) { - this.log.debug('setting up legacy service'); - await this.setupLegacyLogging(setupDeps.http.server); - } - - private async setupLegacyLogging(server: Server) { - const legacyLoggingConfig = await this.legacyLoggingConfig$.pipe(first()).toPromise(); - const currentOpsConfig = await this.opsConfig$.pipe(first()).toPromise(); - - await setupLogging(server, legacyLoggingConfig, currentOpsConfig.interval.asMilliseconds()); - await setupLoggingRotate(server, legacyLoggingConfig); - - this.configSubscription = combineLatest([this.legacyLoggingConfig$, this.opsConfig$]).subscribe( - ([newLoggingConfig, newOpsConfig]) => { - reconfigureLogging(server, newLoggingConfig, newOpsConfig.interval.asMilliseconds()); - } - ); - } - - public async stop() { - this.log.debug('stopping legacy service'); - - if (this.configSubscription !== undefined) { - this.configSubscription.unsubscribe(); - this.configSubscription = undefined; - } - } -} diff --git a/src/core/server/legacy/logging/appenders/__snapshots__/legacy_appender.test.ts.snap b/src/core/server/legacy/logging/appenders/__snapshots__/legacy_appender.test.ts.snap deleted file mode 100644 index 3c40362e8211..000000000000 --- a/src/core/server/legacy/logging/appenders/__snapshots__/legacy_appender.test.ts.snap +++ /dev/null @@ -1,142 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`\`append()\` correctly pushes records to legacy platform. 1`] = ` -Object { - "context": "context-1", - "level": LogLevel { - "id": "trace", - "value": 7, - }, - "message": "message-1", - "pid": Any, - "timestamp": 2012-02-01T11:22:33.044Z, -} -`; - -exports[`\`append()\` correctly pushes records to legacy platform. 2`] = ` -Object { - "context": "context-2", - "level": LogLevel { - "id": "debug", - "value": 6, - }, - "message": "message-2", - "pid": Any, - "timestamp": 2012-02-01T11:22:33.044Z, -} -`; - -exports[`\`append()\` correctly pushes records to legacy platform. 3`] = ` -Object { - "context": "context-3.sub-context-3", - "level": LogLevel { - "id": "info", - "value": 5, - }, - "message": "message-3", - "pid": Any, - "timestamp": 2012-02-01T11:22:33.044Z, -} -`; - -exports[`\`append()\` correctly pushes records to legacy platform. 4`] = ` -Object { - "context": "context-4.sub-context-4", - "level": LogLevel { - "id": "warn", - "value": 4, - }, - "message": "message-4", - "pid": Any, - "timestamp": 2012-02-01T11:22:33.044Z, -} -`; - -exports[`\`append()\` correctly pushes records to legacy platform. 5`] = ` -Object { - "context": "context-5", - "error": [Error: Some Error], - "level": LogLevel { - "id": "error", - "value": 3, - }, - "message": "message-5-with-error", - "pid": Any, - "timestamp": 2012-02-01T11:22:33.044Z, -} -`; - -exports[`\`append()\` correctly pushes records to legacy platform. 6`] = ` -Object { - "context": "context-6", - "level": LogLevel { - "id": "error", - "value": 3, - }, - "message": "message-6-with-message", - "pid": Any, - "timestamp": 2012-02-01T11:22:33.044Z, -} -`; - -exports[`\`append()\` correctly pushes records to legacy platform. 7`] = ` -Object { - "context": "context-7.sub-context-7.sub-sub-context-7", - "error": [Error: Some Fatal Error], - "level": LogLevel { - "id": "fatal", - "value": 2, - }, - "message": "message-7-with-error", - "pid": Any, - "timestamp": 2012-02-01T11:22:33.044Z, -} -`; - -exports[`\`append()\` correctly pushes records to legacy platform. 8`] = ` -Object { - "context": "context-8.sub-context-8.sub-sub-context-8", - "level": LogLevel { - "id": "fatal", - "value": 2, - }, - "message": "message-8-with-message", - "pid": Any, - "timestamp": 2012-02-01T11:22:33.044Z, -} -`; - -exports[`\`append()\` correctly pushes records to legacy platform. 9`] = ` -Object { - "context": "context-9.sub-context-9", - "level": LogLevel { - "id": "info", - "value": 5, - }, - "message": "message-9-with-message", - "meta": Object { - "someValue": 3, - }, - "pid": Any, - "timestamp": 2012-02-01T11:22:33.044Z, -} -`; - -exports[`\`append()\` correctly pushes records to legacy platform. 10`] = ` -Object { - "context": "context-10.sub-context-10", - "level": LogLevel { - "id": "info", - "value": 5, - }, - "message": "message-10-with-message", - "meta": Object { - "tags": Array [ - "tag1", - "tag2", - ], - }, - "pid": Any, - "timestamp": 2012-02-01T11:22:33.044Z, -} -`; diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.test.ts b/src/core/server/legacy/logging/appenders/legacy_appender.test.ts deleted file mode 100644 index 9213403d72d0..000000000000 --- a/src/core/server/legacy/logging/appenders/legacy_appender.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -jest.mock('@kbn/legacy-logging'); - -import { LogRecord, LogLevel } from '../../../logging'; -import { LegacyLoggingServer } from '@kbn/legacy-logging'; -import { LegacyAppender } from './legacy_appender'; - -afterEach(() => (LegacyLoggingServer as any).mockClear()); - -test('`configSchema` creates correct schema.', () => { - const appenderSchema = LegacyAppender.configSchema; - const validConfig = { type: 'legacy-appender', legacyLoggingConfig: { verbose: true } }; - expect(appenderSchema.validate(validConfig)).toEqual({ - type: 'legacy-appender', - legacyLoggingConfig: { verbose: true }, - }); - - const wrongConfig = { type: 'not-legacy-appender' }; - expect(() => appenderSchema.validate(wrongConfig)).toThrow(); -}); - -test('`append()` correctly pushes records to legacy platform.', () => { - const timestamp = new Date(Date.UTC(2012, 1, 1, 11, 22, 33, 44)); - const records: LogRecord[] = [ - { - context: 'context-1', - level: LogLevel.Trace, - message: 'message-1', - timestamp, - pid: 5355, - }, - { - context: 'context-2', - level: LogLevel.Debug, - message: 'message-2', - timestamp, - pid: 5355, - }, - { - context: 'context-3.sub-context-3', - level: LogLevel.Info, - message: 'message-3', - timestamp, - pid: 5355, - }, - { - context: 'context-4.sub-context-4', - level: LogLevel.Warn, - message: 'message-4', - timestamp, - pid: 5355, - }, - { - context: 'context-5', - error: new Error('Some Error'), - level: LogLevel.Error, - message: 'message-5-with-error', - timestamp, - pid: 5355, - }, - { - context: 'context-6', - level: LogLevel.Error, - message: 'message-6-with-message', - timestamp, - pid: 5355, - }, - { - context: 'context-7.sub-context-7.sub-sub-context-7', - error: new Error('Some Fatal Error'), - level: LogLevel.Fatal, - message: 'message-7-with-error', - timestamp, - pid: 5355, - }, - { - context: 'context-8.sub-context-8.sub-sub-context-8', - level: LogLevel.Fatal, - message: 'message-8-with-message', - timestamp, - pid: 5355, - }, - { - context: 'context-9.sub-context-9', - level: LogLevel.Info, - message: 'message-9-with-message', - timestamp, - pid: 5355, - meta: { someValue: 3 }, - }, - { - context: 'context-10.sub-context-10', - level: LogLevel.Info, - message: 'message-10-with-message', - timestamp, - pid: 5355, - meta: { tags: ['tag1', 'tag2'] }, - }, - ]; - - const appender = new LegacyAppender({ verbose: true }); - for (const record of records) { - appender.append(record); - } - - const [mockLegacyLoggingServerInstance] = (LegacyLoggingServer as any).mock.instances; - expect(mockLegacyLoggingServerInstance.log.mock.calls).toHaveLength(records.length); - records.forEach((r, idx) => { - expect(mockLegacyLoggingServerInstance.log.mock.calls[idx][0]).toMatchSnapshot({ - pid: expect.any(Number), - }); - }); -}); - -test('legacy logging server is correctly created and disposed.', async () => { - const mockRawLegacyLoggingConfig = { verbose: true }; - const appender = new LegacyAppender(mockRawLegacyLoggingConfig); - - expect(LegacyLoggingServer).toHaveBeenCalledTimes(1); - expect(LegacyLoggingServer).toHaveBeenCalledWith(mockRawLegacyLoggingConfig); - - const [mockLegacyLoggingServerInstance] = (LegacyLoggingServer as any).mock.instances; - expect(mockLegacyLoggingServerInstance.stop).not.toHaveBeenCalled(); - - await appender.dispose(); - - expect(mockLegacyLoggingServerInstance.stop).toHaveBeenCalledTimes(1); -}); diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.ts b/src/core/server/legacy/logging/appenders/legacy_appender.ts deleted file mode 100644 index 7e02d00c7b23..000000000000 --- a/src/core/server/legacy/logging/appenders/legacy_appender.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { schema } from '@kbn/config-schema'; -import { LegacyLoggingServer } from '@kbn/legacy-logging'; -import { DisposableAppender, LogRecord } from '@kbn/logging'; - -export interface LegacyAppenderConfig { - type: 'legacy-appender'; - legacyLoggingConfig?: Record; -} - -/** - * Simple appender that just forwards `LogRecord` to the legacy KbnServer log. - * @internal - */ -export class LegacyAppender implements DisposableAppender { - public static configSchema = schema.object({ - type: schema.literal('legacy-appender'), - legacyLoggingConfig: schema.recordOf(schema.string(), schema.any()), - }); - - /** - * Sets {@link Appender.receiveAllLevels} because legacy does its own filtering based on the legacy logging - * configuration. - */ - public readonly receiveAllLevels = true; - - private readonly loggingServer: LegacyLoggingServer; - - constructor(legacyLoggingConfig: any) { - this.loggingServer = new LegacyLoggingServer(legacyLoggingConfig); - } - - /** - * Forwards `LogRecord` to the legacy platform that will layout and - * write record to the configured destination. - * @param record `LogRecord` instance to forward to. - */ - public append(record: LogRecord) { - this.loggingServer.log(record); - } - - public dispose() { - this.loggingServer.stop(); - } -} diff --git a/src/core/server/logging/README.mdx b/src/core/server/logging/README.mdx index 08e4ed34204c..11437d1e8df2 100644 --- a/src/core/server/logging/README.mdx +++ b/src/core/server/logging/README.mdx @@ -562,11 +562,6 @@ The log will be less verbose with `warn` level for the `server` context name: ``` ### Logging config migration -Compatibility with the legacy logging system is assured until the end of the `v7` version. -All log messages handled by `root` context are forwarded to the legacy logging service using a `default` appender. If you re-write -root appenders, make sure that it contains `default` appender to provide backward compatibility. -**Note**: If you define an appender for a context name, the log messages for that specific context aren't handled by the -`root` context anymore and not forwarded to the legacy logging service. #### logging.dest By default logs in *stdout*. With new Kibana logging you can use pre-existing `console` appender or diff --git a/src/core/server/logging/appenders/appenders.test.ts b/src/core/server/logging/appenders/appenders.test.ts index bd32e4061049..759fcb9546f0 100644 --- a/src/core/server/logging/appenders/appenders.test.ts +++ b/src/core/server/logging/appenders/appenders.test.ts @@ -9,7 +9,6 @@ import { mockCreateLayout } from './appenders.test.mocks'; import { ByteSizeValue } from '@kbn/config-schema'; -import { LegacyAppender } from '../../legacy/logging/appenders/legacy_appender'; import { Appenders } from './appenders'; import { ConsoleAppender } from './console/console_appender'; import { FileAppender } from './file/file_appender'; @@ -68,13 +67,6 @@ test('`create()` creates correct appender.', () => { }); expect(fileAppender).toBeInstanceOf(FileAppender); - const legacyAppender = Appenders.create({ - type: 'legacy-appender', - legacyLoggingConfig: { verbose: true }, - }); - - expect(legacyAppender).toBeInstanceOf(LegacyAppender); - const rollingFileAppender = Appenders.create({ type: 'rolling-file', fileName: 'path', diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts index 88df355bd5eb..3e867739aa1c 100644 --- a/src/core/server/logging/appenders/appenders.ts +++ b/src/core/server/logging/appenders/appenders.ts @@ -10,10 +10,6 @@ import { schema } from '@kbn/config-schema'; import { assertNever } from '@kbn/std'; import { DisposableAppender } from '@kbn/logging'; -import { - LegacyAppender, - LegacyAppenderConfig, -} from '../../legacy/logging/appenders/legacy_appender'; import { Layouts } from '../layouts/layouts'; import { ConsoleAppender, ConsoleAppenderConfig } from './console/console_appender'; import { FileAppender, FileAppenderConfig } from './file/file_appender'; @@ -32,7 +28,6 @@ import { export const appendersSchema = schema.oneOf([ ConsoleAppender.configSchema, FileAppender.configSchema, - LegacyAppender.configSchema, RewriteAppender.configSchema, RollingFileAppender.configSchema, ]); @@ -41,7 +36,6 @@ export const appendersSchema = schema.oneOf([ export type AppenderConfigType = | ConsoleAppenderConfig | FileAppenderConfig - | LegacyAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig; @@ -64,8 +58,6 @@ export class Appenders { return new RewriteAppender(config); case 'rolling-file': return new RollingFileAppender(config); - case 'legacy-appender': - return new LegacyAppender(config.legacyLoggingConfig); default: return assertNever(config); diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index ade10fc1c025..ff681222c4f3 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -14,7 +14,6 @@ import { Subject } from 'rxjs'; function createRoot() { return kbnTestServer.createRoot({ logging: { - silent: true, // set "true" in kbnTestServer appenders: { 'test-console': { type: 'console', diff --git a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts index 83533e29ad12..dc6a01b80e95 100644 --- a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts +++ b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts @@ -19,7 +19,6 @@ const flush = async () => delay(flushDelay); function createRoot(appenderConfig: any) { return kbnTestServer.createRoot({ logging: { - silent: true, // set "true" in kbnTestServer appenders: { 'rolling-file': appenderConfig, }, diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index e0004ba992c1..41acd072b295 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -9,35 +9,18 @@ import { LoggingConfig, config } from './logging_config'; test('`schema` creates correct schema with defaults.', () => { - expect(config.schema.validate({})).toMatchInlineSnapshot( - { json: expect.any(Boolean) }, // default value depends on TTY - ` + expect(config.schema.validate({})).toMatchInlineSnapshot(` Object { "appenders": Map {}, - "dest": "stdout", - "events": Object {}, - "filter": Object {}, - "json": Any, "loggers": Array [], - "quiet": false, "root": Object { "appenders": Array [ "default", ], "level": "info", }, - "rotate": Object { - "enabled": false, - "everyBytes": 10485760, - "keepFiles": 7, - "pollingInterval": 10000, - "usePolling": false, - }, - "silent": false, - "verbose": false, } - ` - ); + `); }); test('`schema` throws if `root` logger does not have appenders configured.', () => { @@ -52,16 +35,14 @@ test('`schema` throws if `root` logger does not have appenders configured.', () ); }); -test('`schema` throws if `root` logger does not have "default" appender configured.', () => { +test('`schema` does not throw if `root` logger does not have "default" appender configured.', () => { expect(() => config.schema.validate({ root: { appenders: ['console'], }, }) - ).toThrowErrorMatchingInlineSnapshot( - `"[root]: \\"default\\" appender required for migration period till the next major release"` - ); + ).not.toThrow(); }); test('`getParentLoggerContext()` returns correct parent context name.', () => { diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index f5b75d7bb739..a04506ad9c0f 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -7,7 +7,6 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { legacyLoggingConfigSchema } from '@kbn/legacy-logging'; import { AppenderConfigType, Appenders } from './appenders/appenders'; // We need this helper for the types to be correct @@ -58,31 +57,23 @@ export const loggerSchema = schema.object({ /** @public */ export type LoggerConfigType = TypeOf; + export const config = { path: 'logging', - schema: legacyLoggingConfigSchema.extends({ + schema: schema.object({ appenders: schema.mapOf(schema.string(), Appenders.configSchema, { defaultValue: new Map(), }), loggers: schema.arrayOf(loggerSchema, { defaultValue: [], }), - root: schema.object( - { - appenders: schema.arrayOf(schema.string(), { - defaultValue: [DEFAULT_APPENDER_NAME], - minSize: 1, - }), - level: levelSchema, - }, - { - validate(rawConfig) { - if (!rawConfig.appenders.includes(DEFAULT_APPENDER_NAME)) { - return `"${DEFAULT_APPENDER_NAME}" appender required for migration period till the next major release`; - } - }, - } - ), + root: schema.object({ + appenders: schema.arrayOf(schema.string(), { + defaultValue: [DEFAULT_APPENDER_NAME], + minSize: 1, + }), + level: levelSchema, + }), }), }; diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts index dd546d4e7eac..ebe06326f499 100644 --- a/src/core/server/logging/logging_system.test.ts +++ b/src/core/server/logging/logging_system.test.ts @@ -15,11 +15,6 @@ jest.mock('fs', () => ({ const dynamicProps = { process: { pid: expect.any(Number) } }; -jest.mock('@kbn/legacy-logging', () => ({ - ...(jest.requireActual('@kbn/legacy-logging') as any), - setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})), -})); - const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 33, 22, 11)); let mockConsoleLog: jest.SpyInstance; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 1ef845730e1f..770f18d1f7e7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -71,12 +71,11 @@ export interface AppCategory { // Warning: (ae-forgotten-export) The symbol "ConsoleAppenderConfig" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FileAppenderConfig" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "LegacyAppenderConfig" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RewriteAppenderConfig" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RollingFileAppenderConfig" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig; +export type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig; // @public @deprecated export interface AsyncPlugin { diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 47899043dc5a..c4f420f75b5d 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -7,32 +7,30 @@ */ import { httpServiceMock } from './http/http_service.mock'; + export const mockHttpService = httpServiceMock.create(); jest.doMock('./http/http_service', () => ({ HttpService: jest.fn(() => mockHttpService), })); import { pluginServiceMock } from './plugins/plugins_service.mock'; + export const mockPluginsService = pluginServiceMock.create(); jest.doMock('./plugins/plugins_service', () => ({ PluginsService: jest.fn(() => mockPluginsService), })); import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; + export const mockElasticsearchService = elasticsearchServiceMock.create(); jest.doMock('./elasticsearch/elasticsearch_service', () => ({ ElasticsearchService: jest.fn(() => mockElasticsearchService), })); -import { legacyServiceMock } from './legacy/legacy_service.mock'; -export const mockLegacyService = legacyServiceMock.create(); -jest.mock('./legacy/legacy_service', () => ({ - LegacyService: jest.fn(() => mockLegacyService), -})); - const realKbnConfig = jest.requireActual('@kbn/config'); import { configServiceMock } from './config/mocks'; + export const mockConfigService = configServiceMock.create(); jest.doMock('@kbn/config', () => ({ ...realKbnConfig, @@ -40,18 +38,21 @@ jest.doMock('@kbn/config', () => ({ })); import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; + export const mockSavedObjectsService = savedObjectsServiceMock.create(); jest.doMock('./saved_objects/saved_objects_service', () => ({ SavedObjectsService: jest.fn(() => mockSavedObjectsService), })); import { contextServiceMock } from './context/context_service.mock'; + export const mockContextService = contextServiceMock.create(); jest.doMock('./context/context_service', () => ({ ContextService: jest.fn(() => mockContextService), })); import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; + export const mockUiSettingsService = uiSettingsServiceMock.create(); jest.doMock('./ui_settings/ui_settings_service', () => ({ UiSettingsService: jest.fn(() => mockUiSettingsService), @@ -63,46 +64,54 @@ jest.doMock('./config/ensure_valid_configuration', () => ({ })); import { RenderingService, mockRenderingService } from './rendering/__mocks__/rendering_service'; + export { mockRenderingService }; jest.doMock('./rendering/rendering_service', () => ({ RenderingService })); import { environmentServiceMock } from './environment/environment_service.mock'; + export const mockEnvironmentService = environmentServiceMock.create(); jest.doMock('./environment/environment_service', () => ({ EnvironmentService: jest.fn(() => mockEnvironmentService), })); import { metricsServiceMock } from './metrics/metrics_service.mock'; + export const mockMetricsService = metricsServiceMock.create(); jest.doMock('./metrics/metrics_service', () => ({ MetricsService: jest.fn(() => mockMetricsService), })); import { statusServiceMock } from './status/status_service.mock'; + export const mockStatusService = statusServiceMock.create(); jest.doMock('./status/status_service', () => ({ StatusService: jest.fn(() => mockStatusService), })); import { loggingServiceMock } from './logging/logging_service.mock'; + export const mockLoggingService = loggingServiceMock.create(); jest.doMock('./logging/logging_service', () => ({ LoggingService: jest.fn(() => mockLoggingService), })); import { i18nServiceMock } from './i18n/i18n_service.mock'; + export const mockI18nService = i18nServiceMock.create(); jest.doMock('./i18n/i18n_service', () => ({ I18nService: jest.fn(() => mockI18nService), })); import { prebootServiceMock } from './preboot/preboot_service.mock'; + export const mockPrebootService = prebootServiceMock.create(); jest.doMock('./preboot/preboot_service', () => ({ PrebootService: jest.fn(() => mockPrebootService), })); import { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; + export const mockDeprecationService = deprecationsServiceMock.create(); jest.doMock('./deprecations/deprecations_service', () => ({ DeprecationsService: jest.fn(() => mockDeprecationService), diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index b27c8fa769c4..112693aae027 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -9,7 +9,6 @@ import { mockElasticsearchService, mockHttpService, - mockLegacyService, mockPluginsService, mockConfigService, mockSavedObjectsService, @@ -95,7 +94,6 @@ test('sets up services on "setup"', async () => { expect(mockHttpService.setup).not.toHaveBeenCalled(); expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); expect(mockPluginsService.setup).not.toHaveBeenCalled(); - expect(mockLegacyService.setup).not.toHaveBeenCalled(); expect(mockSavedObjectsService.setup).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); @@ -111,7 +109,6 @@ test('sets up services on "setup"', async () => { expect(mockHttpService.setup).toHaveBeenCalledTimes(1); expect(mockElasticsearchService.setup).toHaveBeenCalledTimes(1); expect(mockPluginsService.setup).toHaveBeenCalledTimes(1); - expect(mockLegacyService.setup).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.setup).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.setup).toHaveBeenCalledTimes(1); expect(mockRenderingService.setup).toHaveBeenCalledTimes(1); @@ -199,7 +196,6 @@ test('stops services on "stop"', async () => { expect(mockHttpService.stop).not.toHaveBeenCalled(); expect(mockElasticsearchService.stop).not.toHaveBeenCalled(); expect(mockPluginsService.stop).not.toHaveBeenCalled(); - expect(mockLegacyService.stop).not.toHaveBeenCalled(); expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.stop).not.toHaveBeenCalled(); expect(mockMetricsService.stop).not.toHaveBeenCalled(); @@ -211,7 +207,6 @@ test('stops services on "stop"', async () => { expect(mockHttpService.stop).toHaveBeenCalledTimes(1); expect(mockElasticsearchService.stop).toHaveBeenCalledTimes(1); expect(mockPluginsService.stop).toHaveBeenCalledTimes(1); - expect(mockLegacyService.stop).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.stop).toHaveBeenCalledTimes(1); expect(mockMetricsService.stop).toHaveBeenCalledTimes(1); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 867446484a23..8b0714e89913 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -21,7 +21,6 @@ import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; import { HttpResourcesService } from './http_resources'; import { RenderingService } from './rendering'; -import { LegacyService } from './legacy'; import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging'; import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; @@ -69,7 +68,6 @@ export class Server { private readonly elasticsearch: ElasticsearchService; private readonly http: HttpService; private readonly rendering: RenderingService; - private readonly legacy: LegacyService; private readonly log: Logger; private readonly plugins: PluginsService; private readonly savedObjects: SavedObjectsService; @@ -108,7 +106,6 @@ export class Server { this.http = new HttpService(core); this.rendering = new RenderingService(core); this.plugins = new PluginsService(core); - this.legacy = new LegacyService(core); this.elasticsearch = new ElasticsearchService(core); this.savedObjects = new SavedObjectsService(core); this.uiSettings = new UiSettingsService(core); @@ -286,10 +283,6 @@ export class Server { const pluginsSetup = await this.plugins.setup(coreSetup); this.#pluginsInitialized = pluginsSetup.initialized; - await this.legacy.setup({ - http: httpSetup, - }); - this.registerCoreContext(coreSetup); this.coreApp.setup(coreSetup, uiPlugins); @@ -348,7 +341,6 @@ export class Server { public async stop() { this.log.debug('stopping server'); - await this.legacy.stop(); await this.http.stop(); // HTTP server has to stop before savedObjects and ES clients are closed to be able to gracefully attempt to resolve any pending requests await this.plugins.stop(); await this.savedObjects.stop(); diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 67bd6c7455d6..58720be637e2 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -32,7 +32,11 @@ const DEFAULTS_SETTINGS = { port: 0, xsrf: { disableProtection: true }, }, - logging: { silent: true }, + logging: { + root: { + level: 'off', + }, + }, plugins: {}, migrations: { skip: false }, }; @@ -45,7 +49,6 @@ export function createRootWithSettings( configs: [], cliArgs: { dev: false, - silent: false, watch: false, basePath: false, runExamples: false, diff --git a/src/core/types/elasticsearch/search.ts b/src/core/types/elasticsearch/search.ts index 88d6cda3777d..a54f5f3758ce 100644 --- a/src/core/types/elasticsearch/search.ts +++ b/src/core/types/elasticsearch/search.ts @@ -48,7 +48,7 @@ type ValueTypeOfField = T extends Record type MaybeArray = T | T[]; -type Fields = Exclude['body']['fields'], undefined>; +type Fields = Required['body']>['fields']; type DocValueFields = MaybeArray; export type SearchHit< diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index cee43fd85c90..f671fa963079 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -82,24 +82,13 @@ kibana_vars=( logging.appenders logging.appenders.console logging.appenders.file - logging.dest - logging.json logging.loggers logging.loggers.appenders logging.loggers.level logging.loggers.name - logging.quiet logging.root logging.root.appenders logging.root.level - logging.rotate.enabled - logging.rotate.everyBytes - logging.rotate.keepFiles - logging.rotate.pollingInterval - logging.rotate.usePolling - logging.silent - logging.useUTC - logging.verbose map.includeElasticMapsService map.proxyElasticMapsServiceInMaps map.regionmap diff --git a/src/dev/eslint/lint_files.ts b/src/dev/eslint/lint_files.ts index 77fe941fb7ae..5c6118edeb2e 100644 --- a/src/dev/eslint/lint_files.ts +++ b/src/dev/eslint/lint_files.ts @@ -12,41 +12,6 @@ import { REPO_ROOT } from '@kbn/utils'; import { createFailError, ToolingLog } from '@kbn/dev-utils'; import { File } from '../file'; -// For files living on the filesystem -function lintFilesOnFS(cli: CLIEngine, files: File[]) { - const paths = files.map((file) => file.getRelativePath()); - return cli.executeOnFiles(paths); -} - -// For files living somewhere else (ie. git object) -async function lintFilesOnContent(cli: CLIEngine, files: File[]) { - const report: CLIEngine.LintReport = { - results: [], - errorCount: 0, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0, - usedDeprecatedRules: [], - }; - - for (let i = 0; i < files.length; i++) { - const r = cli.executeOnText(await files[i].getContent(), files[i].getRelativePath()); - // Despite a relative path was given, the result would contain an absolute one. Work around it. - r.results[0].filePath = r.results[0].filePath.replace( - files[i].getAbsolutePath(), - files[i].getRelativePath() - ); - report.results.push(...r.results); - report.errorCount += r.errorCount; - report.warningCount += r.warningCount; - report.fixableErrorCount += r.fixableErrorCount; - report.fixableWarningCount += r.fixableWarningCount; - report.usedDeprecatedRules.push(...r.usedDeprecatedRules); - } - - return report; -} - /** * Lints a list of files with eslint. eslint reports are written to the log * and a FailError is thrown when linting errors occur. @@ -55,16 +20,15 @@ async function lintFilesOnContent(cli: CLIEngine, files: File[]) { * @param {Array} files * @return {undefined} */ -export async function lintFiles(log: ToolingLog, files: File[], { fix }: { fix?: boolean } = {}) { +export function lintFiles(log: ToolingLog, files: File[], { fix }: { fix?: boolean } = {}) { const cli = new CLIEngine({ cache: true, cwd: REPO_ROOT, fix, }); - const virtualFilesCount = files.filter((file) => file.isVirtual()).length; - const report = - virtualFilesCount && !fix ? await lintFilesOnContent(cli, files) : lintFilesOnFS(cli, files); + const paths = files.map((file) => file.getRelativePath()); + const report = cli.executeOnFiles(paths); if (fix) { CLIEngine.outputFixes(report); diff --git a/src/dev/file.ts b/src/dev/file.ts index 01005b257a40..b532a7bb7060 100644 --- a/src/dev/file.ts +++ b/src/dev/file.ts @@ -7,13 +7,11 @@ */ import { dirname, extname, join, relative, resolve, sep, basename } from 'path'; -import { createFailError } from '@kbn/dev-utils'; export class File { private path: string; private relativePath: string; private ext: string; - private fileReader: undefined | (() => Promise); constructor(path: string) { this.path = resolve(path); @@ -57,11 +55,6 @@ export class File { ); } - // Virtual files cannot be read as usual, an helper is needed - public isVirtual() { - return this.fileReader !== undefined; - } - public getRelativeParentDirs() { const parents: string[] = []; @@ -88,15 +81,4 @@ export class File { public toJSON() { return this.relativePath; } - - public setFileReader(fileReader: () => Promise) { - this.fileReader = fileReader; - } - - public getContent() { - if (this.fileReader) { - return this.fileReader(); - } - throw createFailError('getContent() was invoked on a non-virtual File'); - } } diff --git a/src/dev/precommit_hook/get_files_for_commit.js b/src/dev/precommit_hook/get_files_for_commit.js index 52dfab49c5c6..44c8c9d5e6bc 100644 --- a/src/dev/precommit_hook/get_files_for_commit.js +++ b/src/dev/precommit_hook/get_files_for_commit.js @@ -6,65 +6,12 @@ * Side Public License, v 1. */ -import { format } from 'util'; import SimpleGit from 'simple-git'; import { fromNode as fcb } from 'bluebird'; import { REPO_ROOT } from '@kbn/utils'; import { File } from '../file'; -/** - * Return the `git diff` argument used for building the list of files - * - * @param {String} gitRef - * @return {String} - * - * gitRef return - * '' '--cached' - * '' '~1..' - * '..' '..' - * '...' '...' - * '..' '..' - * '...' '...' - * '..' '..' - * '...' '...' - */ -function getRefForDiff(gitRef) { - if (!gitRef) { - return '--cached'; - } else if (gitRef.includes('..')) { - return gitRef; - } else { - return format('%s~1..%s', gitRef, gitRef); - } -} - -/** - * Return the used for reading files content - * - * @param {String} gitRef - * @return {String} - * - * gitRef return - * '' '' - * '' '' - * '..' 'HEAD' - * '...' 'HEAD' - * '..' '' - * '...' '' - * '..' '' - * '...' '' - */ -function getRefForCat(gitRef) { - if (!gitRef) { - return ''; - } else if (gitRef.includes('..')) { - return gitRef.endsWith('..') ? 'HEAD' : gitRef.slice(gitRef.lastIndexOf('..') + 2); - } else { - return gitRef; - } -} - /** * Get the files that are staged for commit (excluding deleted files) * as `File` objects that are aware of their commit status. @@ -74,23 +21,29 @@ function getRefForCat(gitRef) { */ export async function getFilesForCommit(gitRef) { const simpleGit = new SimpleGit(REPO_ROOT); - const gitRefForDiff = getRefForDiff(gitRef); - const gitRefForCat = getRefForCat(gitRef); - - const output = await fcb((cb) => { - simpleGit.diff(['--diff-filter=d', '--name-only', gitRefForDiff], cb); - }); + const gitRefForDiff = gitRef ? gitRef : '--cached'; + const output = await fcb((cb) => simpleGit.diff(['--name-status', gitRefForDiff], cb)); return ( output .split('\n') // Ignore blank lines .filter((line) => line.trim().length > 0) - .map((path) => { - const file = new File(path); - const object = format('%s:%s', gitRefForCat, path); - file.setFileReader(() => fcb((cb) => simpleGit.catFile(['-p', object], cb))); - return file; + // git diff --name-status outputs lines with two OR three parts + // separated by a tab character + .map((line) => line.trim().split('\t')) + .map(([status, ...paths]) => { + // ignore deleted files + if (status === 'D') { + return undefined; + } + + // the status is always in the first column + // .. If the file is edited the line will only have two columns + // .. If the file is renamed it will have three columns + // .. In any case, the last column is the CURRENT path to the file + return new File(paths[paths.length - 1]); }) + .filter(Boolean) ); } diff --git a/src/dev/run_check_published_api_changes.ts b/src/dev/run_check_published_api_changes.ts index 7c8105bc40c5..452922ac56bc 100644 --- a/src/dev/run_check_published_api_changes.ts +++ b/src/dev/run_check_published_api_changes.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog, getTimeReporter } from '@kbn/dev-utils'; import { Extractor, IConfigFile, @@ -27,6 +27,9 @@ const log = new ToolingLog({ writeTo: process.stdout, }); +const runStartTime = Date.now(); +const reportTime = getTimeReporter(log, 'scripts/check_published_api_changes'); + /* * Step 1: execute build:types * This users tsconfig.types.json to generate types in `target/types` @@ -184,6 +187,7 @@ async function run(folder: string, { opts }: { opts: Options }): Promise { + reportTime(runStartTime, 'error', { + success: false, + error: e.message, + }); log.error(e); process.exitCode = 1; }); diff --git a/src/dev/run_i18n_check.ts b/src/dev/run_i18n_check.ts index 48ce2e013fc2..8aa93d33f60f 100644 --- a/src/dev/run_i18n_check.ts +++ b/src/dev/run_i18n_check.ts @@ -9,7 +9,7 @@ import chalk from 'chalk'; import Listr from 'listr'; -import { createFailError, run } from '@kbn/dev-utils'; +import { createFailError, run, ToolingLog, getTimeReporter } from '@kbn/dev-utils'; import { ErrorReporter, I18nConfig } from './i18n'; import { extractDefaultMessages, @@ -19,6 +19,14 @@ import { mergeConfigs, } from './i18n/tasks'; +const toolingLog = new ToolingLog({ + level: 'info', + writeTo: process.stdout, +}); + +const runStartTime = Date.now(); +const reportTime = getTimeReporter(toolingLog, 'scripts/i18n_check'); + const skipOnNoTranslations = ({ config }: { config: I18nConfig }) => !config.translations.length && 'No translations found.'; @@ -116,13 +124,24 @@ run( const reporter = new ErrorReporter(); const messages: Map = new Map(); await list.run({ messages, reporter }); - } catch (error) { + + reportTime(runStartTime, 'total', { + success: true, + }); + } catch (error: Error | ErrorReporter) { process.exitCode = 1; if (error instanceof ErrorReporter) { error.errors.forEach((e: string | Error) => log.error(e)); + reportTime(runStartTime, 'error', { + success: false, + }); } else { log.error('Unhandled exception!'); log.error(error); + reportTime(runStartTime, 'error', { + success: false, + error: error.message, + }); } } }, diff --git a/src/dev/run_precommit_hook.js b/src/dev/run_precommit_hook.js index e1eafaf28d95..a7bd0a9f57f6 100644 --- a/src/dev/run_precommit_hook.js +++ b/src/dev/run_precommit_hook.js @@ -6,7 +6,9 @@ * Side Public License, v 1. */ -import { run, combineErrors, createFlagError, createFailError } from '@kbn/dev-utils'; +import SimpleGit from 'simple-git/promise'; + +import { run, combineErrors, createFlagError, REPO_ROOT } from '@kbn/dev-utils'; import * as Eslint from './eslint'; import * as Stylelint from './stylelint'; import { getFilesForCommit, checkFileCasing } from './precommit_hook'; @@ -23,11 +25,6 @@ run( throw createFlagError('expected --max-files to be a number greater than 0'); } - const virtualFilesCount = files.filter((file) => file.isVirtual()).length; - if (virtualFilesCount > 0 && virtualFilesCount < files.length) { - throw createFailError('Mixing of virtual and on-filesystem files is unsupported'); - } - if (maxFilesCount && files.length > maxFilesCount) { log.warning( `--max-files is set to ${maxFilesCount} and ${files.length} were discovered. The current script execution will be skipped.` @@ -48,6 +45,11 @@ run( await Linter.lintFiles(log, filesToLint, { fix: flags.fix, }); + + if (flags.fix) { + const simpleGit = new SimpleGit(REPO_ROOT); + await simpleGit.add(filesToLint); + } } catch (error) { errors.push(error); } @@ -71,11 +73,7 @@ run( help: ` --fix Execute eslint in --fix mode --max-files Max files number to check against. If exceeded the script will skip the execution - --ref Run checks against git ref files instead of running against staged ones - Examples: - HEAD~1..HEAD files changed in the commit at HEAD - HEAD equivalent to HEAD~1..HEAD - main... files changed in current branch since the common ancestor with main + --ref Run checks against any git ref files (example HEAD or ) instead of running against staged ones `, }, } diff --git a/src/dev/stylelint/lint_files.js b/src/dev/stylelint/lint_files.js index 1ebc98172881..6e62c85d44ae 100644 --- a/src/dev/stylelint/lint_files.js +++ b/src/dev/stylelint/lint_files.js @@ -16,51 +16,6 @@ import { createFailError } from '@kbn/dev-utils'; const stylelintPath = path.resolve(__dirname, '..', '..', '..', '.stylelintrc'); const styleLintConfig = safeLoad(fs.readFileSync(stylelintPath)); -// For files living on the filesystem -function lintFilesOnFS(files) { - const paths = files.map((file) => file.getRelativePath()); - - const options = { - files: paths, - config: styleLintConfig, - formatter: 'string', - ignorePath: path.resolve(__dirname, '..', '..', '..', '.stylelintignore'), - }; - - return stylelint.lint(options); -} - -// For files living somewhere else (ie. git object) -async function lintFilesOnContent(files) { - const report = { - errored: false, - output: '', - postcssResults: [], - results: [], - maxWarningsExceeded: { - maxWarnings: 0, - foundWarnings: 0, - }, - }; - - for (let i = 0; i < files.length; i++) { - const options = { - code: await files[i].getContent(), - config: styleLintConfig, - formatter: 'string', - ignorePath: path.resolve(__dirname, '..', '..', '..', '.stylelintignore'), - }; - const r = await stylelint.lint(options); - report.errored = report.errored || r.errored; - report.output += r.output.replace(//, files[i].getRelativePath()).slice(0, -1); - report.postcssResults.push(...(r.postcssResults || [])); - report.maxWarnings = r.maxWarnings; - report.foundWarnings += r.foundWarnings; - } - - return report; -} - /** * Lints a list of files with eslint. eslint reports are written to the log * and a FailError is thrown when linting errors occur. @@ -70,9 +25,16 @@ async function lintFilesOnContent(files) { * @return {undefined} */ export async function lintFiles(log, files) { - const virtualFilesCount = files.filter((file) => file.isVirtual()).length; - const report = virtualFilesCount ? await lintFilesOnContent(files) : await lintFilesOnFS(files); + const paths = files.map((file) => file.getRelativePath()); + + const options = { + files: paths, + config: styleLintConfig, + formatter: 'string', + ignorePath: path.resolve(__dirname, '..', '..', '..', '.stylelintignore'), + }; + const report = await stylelint.lint(options); if (report.errored) { log.error(report.output); throw createFailError('[stylelint] errors'); diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts index 8abdc36704b4..86a371afd691 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts @@ -12,6 +12,8 @@ import { functionWrapper } from '../../../../expressions/common/expression_funct import { ExpressionValueVisDimension } from '../../../../visualizations/public'; import { Datatable } from '../../../../expressions/common/expression_types/specs'; +type Arguments = Parameters['fn']>[1]; + describe('interpreter/functions#tagcloud', () => { const fn = functionWrapper(tagcloudFunction()); const column1 = 'Count'; @@ -26,7 +28,7 @@ describe('interpreter/functions#tagcloud', () => { { [column1]: 0, [column2]: 'US' }, { [column1]: 10, [column2]: 'UK' }, ], - }; + } as unknown as Datatable; const visConfig = { scale: 'linear', orientation: 'single', @@ -73,12 +75,12 @@ describe('interpreter/functions#tagcloud', () => { }; it('returns an object with the correct structure for number accessors', () => { - const actual = fn(context, { ...visConfig, ...numberAccessors }, undefined); + const actual = fn(context, { ...visConfig, ...numberAccessors } as Arguments, undefined); expect(actual).toMatchSnapshot(); }); it('returns an object with the correct structure for string accessors', () => { - const actual = fn(context, { ...visConfig, ...stringAccessors }, undefined); + const actual = fn(context, { ...visConfig, ...stringAccessors } as Arguments, undefined); expect(actual).toMatchSnapshot(); }); @@ -93,7 +95,7 @@ describe('interpreter/functions#tagcloud', () => { }, }, }; - await fn(context, { ...visConfig, ...numberAccessors }, handlers as any); + await fn(context, { ...visConfig, ...numberAccessors } as Arguments, handlers as any); expect(loggedTable!).toMatchSnapshot(); }); diff --git a/src/plugins/data/common/utils/index.ts b/src/plugins/console/common/constants/index.ts similarity index 81% rename from src/plugins/data/common/utils/index.ts rename to src/plugins/console/common/constants/index.ts index e07fd1859447..0a8dac9b7fff 100644 --- a/src/plugins/data/common/utils/index.ts +++ b/src/plugins/console/common/constants/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -/** @internal */ -export { shortenDottedString } from './shorten_dotted_string'; +export { MAJOR_VERSION } from './plugin'; diff --git a/packages/kbn-legacy-logging/jest.config.js b/src/plugins/console/common/constants/plugin.ts similarity index 75% rename from packages/kbn-legacy-logging/jest.config.js rename to src/plugins/console/common/constants/plugin.ts index d00b1c56dae8..cd301ec29639 100644 --- a/packages/kbn-legacy-logging/jest.config.js +++ b/src/plugins/console/common/constants/plugin.ts @@ -6,8 +6,4 @@ * Side Public License, v 1. */ -module.exports = { - preset: '@kbn/test', - rootDir: '../..', - roots: ['/packages/kbn-legacy-logging'], -}; +export const MAJOR_VERSION = '8.0.0'; diff --git a/src/plugins/console/kibana.json b/src/plugins/console/kibana.json index 69c7176ff6a4..e2345514d76b 100644 --- a/src/plugins/console/kibana.json +++ b/src/plugins/console/kibana.json @@ -1,12 +1,14 @@ { "id": "console", - "version": "kibana", + "version": "8.0.0", + "kibanaVersion": "kibana", "server": true, "ui": true, "owner": { "name": "Stack Management", "githubTeam": "kibana-stack-management" }, + "configPath": ["console"], "requiredPlugins": ["devTools", "share"], "optionalPlugins": ["usageCollection", "home"], "requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils", "home"] diff --git a/src/plugins/console/server/config.ts b/src/plugins/console/server/config.ts index 4e42e3c21d2a..6d667fed081e 100644 --- a/src/plugins/console/server/config.ts +++ b/src/plugins/console/server/config.ts @@ -6,37 +6,70 @@ * Side Public License, v 1. */ +import { SemVer } from 'semver'; import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; -export type ConfigType = TypeOf; +import { MAJOR_VERSION } from '../common/constants'; -export const config = schema.object( - { - enabled: schema.boolean({ defaultValue: true }), - proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }), - ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}), - proxyConfig: schema.arrayOf( - schema.object({ - match: schema.object({ - protocol: schema.string({ defaultValue: '*' }), - host: schema.string({ defaultValue: '*' }), - port: schema.string({ defaultValue: '*' }), - path: schema.string({ defaultValue: '*' }), - }), - - timeout: schema.number(), - ssl: schema.object( - { - verify: schema.boolean(), - ca: schema.arrayOf(schema.string()), - cert: schema.string(), - key: schema.string(), - }, - { defaultValue: undefined } - ), +const kibanaVersion = new SemVer(MAJOR_VERSION); + +const baseSettings = { + enabled: schema.boolean({ defaultValue: true }), + ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}), +}; + +// Settings only available in 7.x +const deprecatedSettings = { + proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }), + proxyConfig: schema.arrayOf( + schema.object({ + match: schema.object({ + protocol: schema.string({ defaultValue: '*' }), + host: schema.string({ defaultValue: '*' }), + port: schema.string({ defaultValue: '*' }), + path: schema.string({ defaultValue: '*' }), }), - { defaultValue: [] } - ), + + timeout: schema.number(), + ssl: schema.object( + { + verify: schema.boolean(), + ca: schema.arrayOf(schema.string()), + cert: schema.string(), + key: schema.string(), + }, + { defaultValue: undefined } + ), + }), + { defaultValue: [] } + ), +}; + +const configSchema = schema.object( + { + ...baseSettings, }, { defaultValue: undefined } ); + +const configSchema7x = schema.object( + { + ...baseSettings, + ...deprecatedSettings, + }, + { defaultValue: undefined } +); + +export type ConfigType = TypeOf; +export type ConfigType7x = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: kibanaVersion.major < 8 ? configSchema7x : configSchema, + deprecations: ({ deprecate, unused }) => [ + deprecate('enabled', '8.0.0'), + deprecate('proxyFilter', '8.0.0'), + deprecate('proxyConfig', '8.0.0'), + unused('ssl'), + ], +}; diff --git a/src/plugins/console/server/index.ts b/src/plugins/console/server/index.ts index cd05652c6283..6ae518f5dc79 100644 --- a/src/plugins/console/server/index.ts +++ b/src/plugins/console/server/index.ts @@ -6,16 +6,11 @@ * Side Public License, v 1. */ -import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; +import { PluginInitializerContext } from 'kibana/server'; -import { ConfigType, config as configSchema } from './config'; import { ConsoleServerPlugin } from './plugin'; export { ConsoleSetup, ConsoleStart } from './types'; +export { config } from './config'; export const plugin = (ctx: PluginInitializerContext) => new ConsoleServerPlugin(ctx); - -export const config: PluginConfigDescriptor = { - deprecations: ({ deprecate, unused, rename }) => [deprecate('enabled', '8.0.0'), unused('ssl')], - schema: configSchema, -}; diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index a5f1ca610760..613337b286fb 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -7,10 +7,11 @@ */ import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; +import { SemVer } from 'semver'; import { ProxyConfigCollection } from './lib'; import { SpecDefinitionsService, EsLegacyConfigService } from './services'; -import { ConfigType } from './config'; +import { ConfigType, ConfigType7x } from './config'; import { registerRoutes } from './routes'; @@ -23,7 +24,7 @@ export class ConsoleServerPlugin implements Plugin { esLegacyConfigService = new EsLegacyConfigService(); - constructor(private readonly ctx: PluginInitializerContext) { + constructor(private readonly ctx: PluginInitializerContext) { this.log = this.ctx.logger.get(); } @@ -34,10 +35,17 @@ export class ConsoleServerPlugin implements Plugin { save: true, }, })); - + const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version); const config = this.ctx.config.get(); const globalConfig = this.ctx.config.legacy.get(); - const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str)); + + let pathFilters: RegExp[] | undefined; + let proxyConfigCollection: ProxyConfigCollection | undefined; + if (kibanaVersion.major < 8) { + // "pathFilters" and "proxyConfig" are only used in 7.x + pathFilters = (config as ConfigType7x).proxyFilter.map((str: string) => new RegExp(str)); + proxyConfigCollection = new ProxyConfigCollection((config as ConfigType7x).proxyConfig); + } this.esLegacyConfigService.setup(elasticsearch.legacy.config$); @@ -51,7 +59,6 @@ export class ConsoleServerPlugin implements Plugin { specDefinitionService: this.specDefinitionsService, }, proxy: { - proxyConfigCollection: new ProxyConfigCollection(config.proxyConfig), readLegacyESConfig: async (): Promise => { const legacyConfig = await this.esLegacyConfigService.readConfig(); return { @@ -59,8 +66,11 @@ export class ConsoleServerPlugin implements Plugin { ...legacyConfig, }; }, - pathFilters: proxyPathFilters, + // Deprecated settings (only used in 7.x): + proxyConfigCollection, + pathFilters, }, + kibanaVersion, }); return { 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 8ca5720d559c..9ece066246e4 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 @@ -9,6 +9,7 @@ import { Agent, IncomingMessage } from 'http'; import * as url from 'url'; import { pick, trimStart, trimEnd } from 'lodash'; +import { SemVer } from 'semver'; import { KibanaRequest, RequestHandler } from 'kibana/server'; @@ -58,17 +59,22 @@ function filterHeaders(originalHeaders: object, headersToKeep: string[]): object function getRequestConfig( headers: object, esConfig: ESConfigForProxy, - proxyConfigCollection: ProxyConfigCollection, - uri: string + uri: string, + kibanaVersion: SemVer, + proxyConfigCollection?: ProxyConfigCollection ): { agent: Agent; timeout: number; headers: object; rejectUnauthorized?: boolean } { const filteredHeaders = filterHeaders(headers, esConfig.requestHeadersWhitelist); const newHeaders = setHeaders(filteredHeaders, esConfig.customHeaders); - if (proxyConfigCollection.hasConfig()) { - return { - ...proxyConfigCollection.configForUri(uri), - headers: newHeaders, - }; + if (kibanaVersion.major < 8) { + // In 7.x we still support the proxyConfig setting defined in kibana.yml + // From 8.x we don't support it anymore so we don't try to read it here. + if (proxyConfigCollection!.hasConfig()) { + return { + ...proxyConfigCollection!.configForUri(uri), + headers: newHeaders, + }; + } } return { @@ -106,18 +112,23 @@ export const createHandler = ({ log, proxy: { readLegacyESConfig, pathFilters, proxyConfigCollection }, + kibanaVersion, }: RouteDependencies): RequestHandler => async (ctx, request, response) => { const { body, query } = request; const { path, method } = query; - if (!pathFilters.some((re) => re.test(path))) { - return response.forbidden({ - body: `Error connecting to '${path}':\n\nUnable to send requests to that path.`, - headers: { - 'Content-Type': 'text/plain', - }, - }); + if (kibanaVersion.major < 8) { + // The "console.proxyFilter" setting in kibana.yaml has been deprecated in 8.x + // We only read it on the 7.x branch + if (!pathFilters!.some((re) => re.test(path))) { + return response.forbidden({ + body: `Error connecting to '${path}':\n\nUnable to send requests to that path.`, + headers: { + 'Content-Type': 'text/plain', + }, + }); + } } const legacyConfig = await readLegacyESConfig(); @@ -134,8 +145,9 @@ export const createHandler = const { timeout, agent, headers, rejectUnauthorized } = getRequestConfig( request.headers, legacyConfig, - proxyConfigCollection, - uri.toString() + uri.toString(), + kibanaVersion, + proxyConfigCollection ); const requestHeaders = { diff --git a/src/plugins/console/server/routes/api/console/proxy/mocks.ts b/src/plugins/console/server/routes/api/console/proxy/mocks.ts index 010e35ab505a..d06ca90adf55 100644 --- a/src/plugins/console/server/routes/api/console/proxy/mocks.ts +++ b/src/plugins/console/server/routes/api/console/proxy/mocks.ts @@ -5,28 +5,41 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { SemVer } from 'semver'; jest.mock('../../../../lib/proxy_request', () => ({ proxyRequest: jest.fn(), })); import { duration } from 'moment'; +import { MAJOR_VERSION } from '../../../../../common/constants'; import { ProxyConfigCollection } from '../../../../lib'; import { RouteDependencies, ProxyDependencies } from '../../../../routes'; import { EsLegacyConfigService, SpecDefinitionsService } from '../../../../services'; import { coreMock, httpServiceMock } from '../../../../../../../core/server/mocks'; -const defaultProxyValue = Object.freeze({ - readLegacyESConfig: async () => ({ - requestTimeout: duration(30000), - customHeaders: {}, - requestHeadersWhitelist: [], - hosts: ['http://localhost:9200'], - }), - pathFilters: [/.*/], - proxyConfigCollection: new ProxyConfigCollection([]), +const kibanaVersion = new SemVer(MAJOR_VERSION); + +const readLegacyESConfig = async () => ({ + requestTimeout: duration(30000), + customHeaders: {}, + requestHeadersWhitelist: [], + hosts: ['http://localhost:9200'], +}); + +let defaultProxyValue = Object.freeze({ + readLegacyESConfig, }); +if (kibanaVersion.major < 8) { + // In 7.x we still support the "pathFilter" and "proxyConfig" kibana.yml settings + defaultProxyValue = Object.freeze({ + readLegacyESConfig, + pathFilters: [/.*/], + proxyConfigCollection: new ProxyConfigCollection([]), + }); +} + interface MockDepsArgument extends Partial> { proxy?: Partial; } @@ -51,5 +64,6 @@ export const getProxyRouteHandlerDeps = ({ } : defaultProxyValue, log, + kibanaVersion, }; }; diff --git a/src/plugins/console/server/routes/api/console/proxy/params.test.ts b/src/plugins/console/server/routes/api/console/proxy/params.test.ts index e08d2f8adecb..edefb2f11f1f 100644 --- a/src/plugins/console/server/routes/api/console/proxy/params.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/params.test.ts @@ -5,14 +5,17 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { SemVer } from 'semver'; import { kibanaResponseFactory } from '../../../../../../../core/server'; -import { getProxyRouteHandlerDeps } from './mocks'; -import { createResponseStub } from './stubs'; +import { getProxyRouteHandlerDeps } from './mocks'; // import need to come first +import { createResponseStub } from './stubs'; // import needs to come first +import { MAJOR_VERSION } from '../../../../../common/constants'; import * as requestModule from '../../../../lib/proxy_request'; - import { createHandler } from './create_handler'; +const kibanaVersion = new SemVer(MAJOR_VERSION); + describe('Console Proxy Route', () => { let handler: ReturnType; @@ -21,58 +24,71 @@ describe('Console Proxy Route', () => { }); describe('params', () => { - describe('pathFilters', () => { - describe('no matches', () => { - it('rejects with 403', async () => { - handler = createHandler( - getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } }) - ); + if (kibanaVersion.major < 8) { + describe('pathFilters', () => { + describe('no matches', () => { + it('rejects with 403', async () => { + handler = createHandler( + getProxyRouteHandlerDeps({ + proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] }, + }) + ); - const { status } = await handler( - {} as any, - { query: { method: 'POST', path: '/baz/id' } } as any, - kibanaResponseFactory - ); + const { status } = await handler( + {} as any, + { query: { method: 'POST', path: '/baz/id' } } as any, + kibanaResponseFactory + ); - expect(status).toBe(403); + expect(status).toBe(403); + }); }); - }); - describe('one match', () => { - it('allows the request', async () => { - handler = createHandler( - getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } }) - ); - (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo')); + describe('one match', () => { + it('allows the request', async () => { + handler = createHandler( + getProxyRouteHandlerDeps({ + proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] }, + }) + ); + + (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo')); - const { status } = await handler( - {} as any, - { headers: {}, query: { method: 'POST', path: '/foo/id' } } as any, - kibanaResponseFactory - ); + const { status } = await handler( + {} as any, + { headers: {}, query: { method: 'POST', path: '/foo/id' } } as any, + kibanaResponseFactory + ); - expect(status).toBe(200); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); + expect(status).toBe(200); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); + }); }); - }); - describe('all match', () => { - it('allows the request', async () => { - handler = createHandler( - getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//] } }) - ); - (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo')); + describe('all match', () => { + it('allows the request', async () => { + handler = createHandler( + getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//] } }) + ); + + (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo')); - const { status } = await handler( - {} as any, - { headers: {}, query: { method: 'GET', path: '/foo/id' } } as any, - kibanaResponseFactory - ); + const { status } = await handler( + {} as any, + { headers: {}, query: { method: 'GET', path: '/foo/id' } } as any, + kibanaResponseFactory + ); - expect(status).toBe(200); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); + expect(status).toBe(200); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); + }); }); }); - }); + } else { + // jest requires to have at least one test in the file + test('dummy required test', () => { + expect(true).toBe(true); + }); + } }); }); diff --git a/src/plugins/console/server/routes/index.ts b/src/plugins/console/server/routes/index.ts index 2c46547f92f1..3911e8cfabc6 100644 --- a/src/plugins/console/server/routes/index.ts +++ b/src/plugins/console/server/routes/index.ts @@ -7,6 +7,7 @@ */ import { IRouter, Logger } from 'kibana/server'; +import { SemVer } from 'semver'; import { EsLegacyConfigService, SpecDefinitionsService } from '../services'; import { ESConfigForProxy } from '../types'; @@ -18,8 +19,8 @@ import { registerSpecDefinitionsRoute } from './api/console/spec_definitions'; export interface ProxyDependencies { readLegacyESConfig: () => Promise; - pathFilters: RegExp[]; - proxyConfigCollection: ProxyConfigCollection; + pathFilters?: RegExp[]; // Only present in 7.x + proxyConfigCollection?: ProxyConfigCollection; // Only present in 7.x } export interface RouteDependencies { @@ -30,6 +31,7 @@ export interface RouteDependencies { esLegacyConfigService: EsLegacyConfigService; specDefinitionService: SpecDefinitionsService; }; + kibanaVersion: SemVer; } export const registerRoutes = (dependencies: RouteDependencies) => { diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx index 0ddd0902b719..46ae4d9456d9 100644 --- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx @@ -128,7 +128,10 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => { name: titleInWizard || title, icon: icon as string, onClick: - group === VisGroups.AGGBASED ? createNewAggsBasedVis(visType) : createNewVisType(visType), + // not all the agg-based visualizations need to be created via the wizard + group === VisGroups.AGGBASED && visType.options.showIndexSelection + ? createNewAggsBasedVis(visType) + : createNewVisType(visType), 'data-test-subj': `visType-${name}`, toolTipContent: description, }; diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx index 40e82d3034ee..e24a949a0c2e 100644 --- a/src/plugins/data/README.mdx +++ b/src/plugins/data/README.mdx @@ -1,6 +1,6 @@ --- id: kibDataPlugin -slug: /kibana-dev-docs/services/data-plugin +slug: /kibana-dev-docs/key-concepts/data-plugin title: Data services image: https://source.unsplash.com/400x175/?Search summary: The data plugin contains services for searching, querying and filtering. @@ -49,15 +49,6 @@ This is helpful when you want to provide a user with options, for example when c ``` -## Data Views - -The data views API provides a consistent method of structuring and formatting documents -and field lists across the various Kibana apps. Its typically used in conjunction with - for composing queries. - -*Note: Kibana index patterns are currently being renamed to data views. There will be some naming inconsistencies until the transition is complete.* - - ## Query The query service is responsible for managing the configuration of a search query (`QueryState`): filters, time range, query string, and settings such as the auto refresh behavior and saved queries. diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 2c339d140823..c236be18a8e4 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -9,15 +9,6 @@ export const DEFAULT_QUERY_LANGUAGE = 'kuery'; export const KIBANA_USER_QUERY_LANGUAGE_KEY = 'kibana.userQueryLanguage'; -/** @public **/ -export const DATA_VIEW_SAVED_OBJECT_TYPE = 'index-pattern'; - -/** - * @deprecated Use DATA_VIEW_SAVED_OBJECT_TYPE. All index pattern interfaces were renamed. - */ - -export const INDEX_PATTERN_SAVED_OBJECT_TYPE = DATA_VIEW_SAVED_OBJECT_TYPE; - export type ValueSuggestionsMethod = 'terms_enum' | 'terms_agg'; export const UI_SETTINGS = { diff --git a/src/plugins/data/common/data_views/index.ts b/src/plugins/data/common/data_views/index.ts deleted file mode 100644 index fd1df0336815..000000000000 --- a/src/plugins/data/common/data_views/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export * from './constants'; -export * from './fields'; -export * from './types'; -export { - IndexPatternsService, - IndexPatternsContract, - DataViewsService, - DataViewsContract, -} from './data_views'; -// todo was trying to export this as type but wasn't working -export { IndexPattern, IndexPatternListItem, DataView, DataViewListItem } from './data_views'; -export * from './errors'; -export * from './expressions'; diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 8a9a93f96b68..195cb9d47531 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -11,18 +11,68 @@ export * from './constants'; export * from './es_query'; -export * from './data_views'; export * from './kbn_field_types'; export * from './query'; export * from './search'; export * from './types'; -export * from './utils'; export * from './exports'; - -/** - * - * @deprecated Use data plugin interface instead - * @removeBy 8.1 - */ - -export { IndexPatternAttributes } from './types'; +export type { + IFieldType, + IIndexPatternFieldList, + FieldFormatMap, + RuntimeType, + RuntimeField, + IIndexPattern, + DataViewAttributes, + IndexPatternAttributes, + FieldAttrs, + FieldAttrSet, + OnNotification, + OnError, + UiSettingsCommon, + SavedObjectsClientCommonFindArgs, + SavedObjectsClientCommon, + GetFieldsOptions, + GetFieldsOptionsTimePattern, + IDataViewsApiClient, + IIndexPatternsApiClient, + SavedObject, + AggregationRestrictions, + TypeMeta, + FieldSpecConflictDescriptions, + FieldSpecExportFmt, + FieldSpec, + DataViewFieldMap, + IndexPatternFieldMap, + DataViewSpec, + IndexPatternSpec, + SourceFilter, + IndexPatternExpressionType, + IndexPatternLoadStartDependencies, + IndexPatternLoadExpressionFunctionDefinition, +} from '../../data_views/common'; +export { + RUNTIME_FIELD_TYPES, + FLEET_ASSETS_TO_IGNORE, + META_FIELDS, + DATA_VIEW_SAVED_OBJECT_TYPE, + INDEX_PATTERN_SAVED_OBJECT_TYPE, + isFilterable, + isNestedField, + fieldList, + DataViewField, + IndexPatternField, + DataViewType, + IndexPatternType, + IndexPatternsService, + IndexPatternsContract, + DataViewsService, + DataViewsContract, + IndexPattern, + IndexPatternListItem, + DataView, + DataViewListItem, + DuplicateDataViewError, + DataViewSavedObjectConflictError, + getIndexPatternLoadMeta, +} from '../../data_views/common'; diff --git a/src/plugins/data/common/mocks.ts b/src/plugins/data/common/mocks.ts index 66ad3b695d24..c656d9d21346 100644 --- a/src/plugins/data/common/mocks.ts +++ b/src/plugins/data/common/mocks.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export * from './data_views/fields/fields.mocks'; +export * from '../../data_views/common/fields/fields.mocks'; diff --git a/src/plugins/data/common/search/aggs/param_types/field.ts b/src/plugins/data/common/search/aggs/param_types/field.ts index e1c872ac1670..05e1302475d0 100644 --- a/src/plugins/data/common/search/aggs/param_types/field.ts +++ b/src/plugins/data/common/search/aggs/param_types/field.ts @@ -15,7 +15,7 @@ import { import { BaseParamType } from './base'; import { propFilter } from '../utils'; import { KBN_FIELD_TYPES } from '../../../kbn_field_types/types'; -import { isNestedField, IndexPatternField } from '../../../data_views/fields'; +import { isNestedField, IndexPatternField } from '../../../../../data_views/common'; const filterByType = propFilter('type'); diff --git a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts index a9078d6042db..09d68177c3ec 100644 --- a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts +++ b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts @@ -12,7 +12,7 @@ import { Observable } from 'rxjs'; import type { Datatable, ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { buildExpressionFunction } from '../../../../../../plugins/expressions/common'; -import { IndexPatternExpressionType } from '../../../data_views/expressions'; +import { IndexPatternExpressionType } from '../../../../../data_views/common/expressions'; import { IndexPatternsContract } from '../../..'; import { AggsStart, AggExpressionType, aggCountFnName } from '../../aggs'; diff --git a/src/plugins/data/common/search/search_source/extract_references.ts b/src/plugins/data/common/search/search_source/extract_references.ts index c7f6c53d0f5f..dfcd1b12cb62 100644 --- a/src/plugins/data/common/search/search_source/extract_references.ts +++ b/src/plugins/data/common/search/search_source/extract_references.ts @@ -10,7 +10,7 @@ import { SavedObjectReference } from 'src/core/types'; import { Filter } from '@kbn/es-query'; import { SearchSourceFields } from './types'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../constants'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../../data/common'; export const extractReferences = ( state: SearchSourceFields @@ -22,7 +22,7 @@ export const extractReferences = ( const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; references.push({ name: refName, - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, id: indexId, }); searchSourceFields = { @@ -42,7 +42,7 @@ export const extractReferences = ( const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; references.push({ name: refName, - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, id: filterRow.meta.index, }); return { diff --git a/src/plugins/data/common/stubs.ts b/src/plugins/data/common/stubs.ts index 5cddcf397f44..ec53b20f6ff3 100644 --- a/src/plugins/data/common/stubs.ts +++ b/src/plugins/data/common/stubs.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export * from './data_views/field.stub'; -export * from './data_views/data_view.stub'; +export * from '../../data_views/common/field.stub'; +export * from '../../data_views/common/data_view.stub'; export * from './es_query/stubs'; diff --git a/src/plugins/data/common/types.ts b/src/plugins/data/common/types.ts index c574d4854cfd..81b47735d8fe 100644 --- a/src/plugins/data/common/types.ts +++ b/src/plugins/data/common/types.ts @@ -8,7 +8,6 @@ export * from './query/types'; export * from './kbn_field_types/types'; -export * from './data_views/types'; /** * If a service is being shared on both the client and the server, and diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index c21955501787..3d70d138d80e 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -3,8 +3,8 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["bfetch", "expressions", "uiActions", "share", "inspector", "fieldFormats"], - "serviceFolders": ["search", "data_views", "query", "autocomplete", "ui"], + "requiredPlugins": ["bfetch", "expressions", "uiActions", "share", "inspector", "fieldFormats", "dataViews"], + "serviceFolders": ["search", "query", "autocomplete", "ui"], "optionalPlugins": ["usageCollection"], "extraPublicDirs": ["common"], "requiredBundles": ["kibanaUtils", "kibanaReact", "inspector"], diff --git a/src/plugins/data/public/data_views/index.ts b/src/plugins/data/public/data_views/index.ts index 0125b173989f..4fb2bbaf0850 100644 --- a/src/plugins/data/public/data_views/index.ts +++ b/src/plugins/data/public/data_views/index.ts @@ -6,25 +6,4 @@ * Side Public License, v 1. */ -export { - ILLEGAL_CHARACTERS_KEY, - CONTAINS_SPACES_KEY, - ILLEGAL_CHARACTERS_VISIBLE, - ILLEGAL_CHARACTERS, - validateDataView, -} from '../../common/data_views/lib'; -export { flattenHitWrapper, formatHitProvider, onRedirectNoIndexPattern } from './data_views'; - -export { IndexPatternField, IIndexPatternFieldList, TypeMeta } from '../../common/data_views'; - -export { - IndexPatternsService, - IndexPatternsContract, - IndexPattern, - DataViewsApiClient, - DataViewsService, - DataViewsContract, - DataView, -} from './data_views'; -export { UiSettingsPublicToCommon } from './ui_settings_wrapper'; -export { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper'; +export * from '../../../data_views/public'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e1f5b98baca9..4b6d184f807a 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -83,14 +83,12 @@ export { IndexPatternLoadExpressionFunctionDefinition, fieldList, GetFieldsOptions, - INDEX_PATTERN_SAVED_OBJECT_TYPE, AggregationRestrictions, IndexPatternType, IndexPatternListItem, + DuplicateDataViewError, } from '../common'; -export { DuplicateDataViewError } from '../common/data_views/errors'; - /* * Autocomplete query suggestions: */ diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index aa766f78a5ec..4a55cc2a0d51 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -21,12 +21,6 @@ import { AutocompleteService } from './autocomplete'; import { SearchService } from './search/search_service'; import { QueryService } from './query'; import { createIndexPatternSelect } from './ui/index_pattern_select'; -import { - DataViewsService, - onRedirectNoIndexPattern, - DataViewsApiClient, - UiSettingsPublicToCommon, -} from './data_views'; import { setIndexPatterns, setNotifications, @@ -44,8 +38,6 @@ import { createSelectRangeAction, } from './actions'; import { APPLY_FILTER_TRIGGER, applyFilterTrigger } from './triggers'; -import { SavedObjectsClientPublicToCommon } from './data_views'; -import { getIndexPatternLoad } from './data_views/expressions'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { getTableViewDescription } from './utils/table_inspector_view'; import { NowProvider, NowProviderInternalContract } from './now_provider'; @@ -89,8 +81,6 @@ export class DataPublicPlugin ): DataPublicPluginSetup { const startServices = createStartServicesGetter(core.getStartServices); - expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices })); - this.usageCollection = usageCollection; const searchService = this.searchService.setup(core, { @@ -138,29 +128,13 @@ export class DataPublicPlugin public start( core: CoreStart, - { uiActions, fieldFormats }: DataStartDependencies + { uiActions, fieldFormats, dataViews }: DataStartDependencies ): DataPublicPluginStart { - const { uiSettings, http, notifications, savedObjects, overlays, application } = core; + const { uiSettings, notifications, savedObjects, overlays } = core; setNotifications(notifications); setOverlays(overlays); setUiSettings(uiSettings); - - const indexPatterns = new DataViewsService({ - uiSettings: new UiSettingsPublicToCommon(uiSettings), - savedObjectsClient: new SavedObjectsClientPublicToCommon(savedObjects.client), - apiClient: new DataViewsApiClient(http), - fieldFormats, - onNotification: (toastInputFields) => { - notifications.toasts.add(toastInputFields); - }, - onError: notifications.toasts.addError.bind(notifications.toasts), - onRedirectNoIndexPattern: onRedirectNoIndexPattern( - application.capabilities, - application.navigateToApp, - overlays - ), - }); - setIndexPatterns(indexPatterns); + setIndexPatterns(dataViews); const query = this.queryService.start({ storage: this.storage, @@ -168,7 +142,7 @@ export class DataPublicPlugin uiSettings, }); - const search = this.searchService.start(core, { fieldFormats, indexPatterns }); + const search = this.searchService.start(core, { fieldFormats, indexPatterns: dataViews }); setSearchService(search); uiActions.addTriggerAction( @@ -197,8 +171,8 @@ export class DataPublicPlugin }, autocomplete: this.autocomplete.start(), fieldFormats, - indexPatterns, - dataViews: indexPatterns, + indexPatterns: dataViews, + dataViews, query, search, nowProvider: this.nowProvider, @@ -214,7 +188,7 @@ export class DataPublicPlugin return { ...dataServices, ui: { - IndexPatternSelect: createIndexPatternSelect(indexPatterns), + IndexPatternSelect: createIndexPatternSelect(dataViews), SearchBar, }, }; diff --git a/src/plugins/data/public/stubs.ts b/src/plugins/data/public/stubs.ts index 3d160a56bd8c..b81d9c4cc78e 100644 --- a/src/plugins/data/public/stubs.ts +++ b/src/plugins/data/public/stubs.ts @@ -7,4 +7,5 @@ */ export * from '../common/stubs'; -export { createStubDataView } from './data_views/data_views/data_view.stub'; +// eslint-disable-next-line +export { createStubDataView } from '../../data_views/public/data_views/data_view.stub'; diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 7ed13c209651..9b16ee39f5c8 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -11,6 +11,7 @@ import { CoreStart } from 'src/core/public'; import { BfetchPublicSetup } from 'src/plugins/bfetch/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; +import { DataViewsPublicPluginStart } from 'src/plugins/data_views/public'; import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { FieldFormatsSetup, FieldFormatsStart } from 'src/plugins/field_formats/public'; import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; @@ -35,6 +36,7 @@ export interface DataSetupDependencies { export interface DataStartDependencies { uiActions: UiActionsStart; fieldFormats: FieldFormatsStart; + dataViews: DataViewsPublicPluginStart; } /** diff --git a/src/plugins/data/server/data_views/index.ts b/src/plugins/data/server/data_views/index.ts index 7226d6f015cf..91a61f4bcb7d 100644 --- a/src/plugins/data/server/data_views/index.ts +++ b/src/plugins/data/server/data_views/index.ts @@ -6,12 +6,4 @@ * Side Public License, v 1. */ -export * from './utils'; -export { - IndexPatternsFetcher, - FieldDescriptor, - shouldReadFieldFromDocValues, - mergeCapabilitiesWithFields, - getCapabilitiesForRollupIndices, -} from './fetcher'; -export { IndexPatternsServiceProvider, IndexPatternsServiceStart } from './index_patterns_service'; +export * from '../../../data_views/server'; diff --git a/src/plugins/data/server/data_views/index_patterns_service.ts b/src/plugins/data/server/data_views/index_patterns_service.ts deleted file mode 100644 index 5286d1d64794..000000000000 --- a/src/plugins/data/server/data_views/index_patterns_service.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - CoreSetup, - CoreStart, - Plugin, - Logger, - SavedObjectsClientContract, - ElasticsearchClient, - UiSettingsServiceStart, -} from 'kibana/server'; -import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { DataPluginStart } from '../plugin'; -import { registerRoutes } from './routes'; -import { indexPatternSavedObjectType } from '../saved_objects'; -import { capabilitiesProvider } from './capabilities_provider'; -import { IndexPatternsCommonService } from '../'; -import { FieldFormatsStart } from '../../../field_formats/server'; -import { getIndexPatternLoad } from './expressions'; -import { UiSettingsServerToCommon } from './ui_settings_wrapper'; -import { IndexPatternsApiServer } from './index_patterns_api_client'; -import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper'; -import { registerIndexPatternsUsageCollector } from './register_index_pattern_usage_collection'; -import { createScriptedFieldsDeprecationsConfig } from './deprecations'; - -export interface IndexPatternsServiceStart { - indexPatternsServiceFactory: ( - savedObjectsClient: SavedObjectsClientContract, - elasticsearchClient: ElasticsearchClient - ) => Promise; -} - -export interface IndexPatternsServiceSetupDeps { - expressions: ExpressionsServerSetup; - logger: Logger; - usageCollection?: UsageCollectionSetup; -} - -export interface IndexPatternsServiceStartDeps { - fieldFormats: FieldFormatsStart; - logger: Logger; -} - -export const indexPatternsServiceFactory = - ({ - logger, - uiSettings, - fieldFormats, - }: { - logger: Logger; - uiSettings: UiSettingsServiceStart; - fieldFormats: FieldFormatsStart; - }) => - async ( - savedObjectsClient: SavedObjectsClientContract, - elasticsearchClient: ElasticsearchClient - ) => { - const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); - const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); - - return new IndexPatternsCommonService({ - uiSettings: new UiSettingsServerToCommon(uiSettingsClient), - savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), - apiClient: new IndexPatternsApiServer(elasticsearchClient, savedObjectsClient), - fieldFormats: formats, - onError: (error) => { - logger.error(error); - }, - onNotification: ({ title, text }) => { - logger.warn(`${title}${text ? ` : ${text}` : ''}`); - }, - }); - }; - -export class IndexPatternsServiceProvider implements Plugin { - public setup( - core: CoreSetup, - { expressions, usageCollection }: IndexPatternsServiceSetupDeps - ) { - core.savedObjects.registerType(indexPatternSavedObjectType); - core.capabilities.registerProvider(capabilitiesProvider); - - registerRoutes(core.http, core.getStartServices); - - expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices })); - registerIndexPatternsUsageCollector(core.getStartServices, usageCollection); - core.deprecations.registerDeprecations(createScriptedFieldsDeprecationsConfig(core)); - } - - public start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps) { - const { uiSettings } = core; - - return { - indexPatternsServiceFactory: indexPatternsServiceFactory({ - logger, - uiSettings, - fieldFormats, - }), - }; - } -} diff --git a/src/plugins/data/server/data_views/mocks.ts b/src/plugins/data/server/data_views/mocks.ts index 6435c09cb7ec..69b57ed07912 100644 --- a/src/plugins/data/server/data_views/mocks.ts +++ b/src/plugins/data/server/data_views/mocks.ts @@ -6,8 +6,4 @@ * Side Public License, v 1. */ -export function createIndexPatternsStartMock() { - return { - indexPatternsServiceFactory: jest.fn().mockResolvedValue({ get: jest.fn() }), - }; -} +export * from '../../../data_views/server/mocks'; diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index a17c66c694b2..fce73e65dc69 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -30,7 +30,7 @@ export const exporters = { * Field Formats: */ -export { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../common'; +export { DATA_VIEW_SAVED_OBJECT_TYPE } from '../common'; /* * Index patterns: diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 3342519782d7..cb52500e78f9 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -9,8 +9,8 @@ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; +import { PluginStart as DataViewsServerPluginStart } from 'src/plugins/data_views/server'; import { ConfigSchema } from '../config'; -import { IndexPatternsServiceProvider, IndexPatternsServiceStart } from './data_views'; import { ISearchSetup, ISearchStart, SearchEnhancements } from './search'; import { SearchService } from './search/search_service'; import { QueryService } from './query/query_service'; @@ -43,7 +43,7 @@ export interface DataPluginStart { * @deprecated - use "fieldFormats" plugin directly instead */ fieldFormats: FieldFormatsStart; - indexPatterns: IndexPatternsServiceStart; + indexPatterns: DataViewsServerPluginStart; } export interface DataPluginSetupDependencies { @@ -56,6 +56,7 @@ export interface DataPluginSetupDependencies { export interface DataPluginStartDependencies { fieldFormats: FieldFormatsStart; logger: Logger; + dataViews: DataViewsServerPluginStart; } export class DataServerPlugin @@ -71,7 +72,6 @@ export class DataServerPlugin private readonly scriptsService: ScriptsService; private readonly kqlTelemetryService: KqlTelemetryService; private readonly autocompleteService: AutocompleteService; - private readonly indexPatterns = new IndexPatternsServiceProvider(); private readonly queryService = new QueryService(); private readonly logger: Logger; @@ -91,11 +91,6 @@ export class DataServerPlugin this.queryService.setup(core); this.autocompleteService.setup(core); this.kqlTelemetryService.setup(core, { usageCollection }); - this.indexPatterns.setup(core, { - expressions, - logger: this.logger.get('indexPatterns'), - usageCollection, - }); core.uiSettings.register(getUiSettings()); @@ -114,16 +109,11 @@ export class DataServerPlugin }; } - public start(core: CoreStart, { fieldFormats }: DataPluginStartDependencies) { - const indexPatterns = this.indexPatterns.start(core, { - fieldFormats, - logger: this.logger.get('indexPatterns'), - }); - + public start(core: CoreStart, { fieldFormats, dataViews }: DataPluginStartDependencies) { return { fieldFormats, - indexPatterns, - search: this.searchService.start(core, { fieldFormats, indexPatterns }), + indexPatterns: dataViews, + search: this.searchService.start(core, { fieldFormats, indexPatterns: dataViews }), }; } diff --git a/src/plugins/data/server/saved_objects/index.ts b/src/plugins/data/server/saved_objects/index.ts index 53d4360782b5..8bfce1a4d369 100644 --- a/src/plugins/data/server/saved_objects/index.ts +++ b/src/plugins/data/server/saved_objects/index.ts @@ -7,6 +7,5 @@ */ export { querySavedObjectType } from './query'; -export { indexPatternSavedObjectType } from './index_patterns'; export { kqlTelemetry } from './kql_telemetry'; export { searchTelemetry } from './search_telemetry'; diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json index 3687604e05e2..92f80f47eca6 100644 --- a/src/plugins/data/tsconfig.json +++ b/src/plugins/data/tsconfig.json @@ -23,6 +23,7 @@ { "path": "../usage_collection/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, - { "path": "../field_formats/tsconfig.json" } + { "path": "../field_formats/tsconfig.json" }, + { "path": "../data_views/tsconfig.json" } ] } diff --git a/src/plugins/data_views/README.mdx b/src/plugins/data_views/README.mdx new file mode 100644 index 000000000000..90efdc18d8fd --- /dev/null +++ b/src/plugins/data_views/README.mdx @@ -0,0 +1,19 @@ +--- +id: kibDataPlugin +slug: /kibana-dev-docs/services/data-plugin +title: Data services +image: https://source.unsplash.com/400x175/?Search +summary: The data plugin contains services for searching, querying and filtering. +date: 2020-12-02 +tags: ['kibana', 'dev', 'contributor', 'api docs'] +--- + +# Data Views + +The data views API provides a consistent method of structuring and formatting documents +and field lists across the various Kibana apps. Its typically used in conjunction with + for composing queries. + +*Note: Kibana index patterns are currently being renamed to data views. There will be some naming inconsistencies until the transition is complete.* + + diff --git a/src/plugins/data/common/data_views/constants.ts b/src/plugins/data_views/common/constants.ts similarity index 81% rename from src/plugins/data/common/data_views/constants.ts rename to src/plugins/data_views/common/constants.ts index 67e266dbd84a..ca42221806b2 100644 --- a/src/plugins/data/common/data_views/constants.ts +++ b/src/plugins/data_views/common/constants.ts @@ -29,3 +29,14 @@ export const FLEET_ASSETS_TO_IGNORE = { METRICS_DATA_STREAM_TO_IGNORE: 'metrics-elastic_agent', // ignore ds created by Fleet server itself METRICS_ENDPOINT_INDEX_TO_IGNORE: 'metrics-endpoint.metadata_current_default', // ignore index created by Fleet endpoint package installed by default in Cloud }; + +export const META_FIELDS = 'metaFields'; + +/** @public **/ +export const DATA_VIEW_SAVED_OBJECT_TYPE = 'index-pattern'; + +/** + * @deprecated Use DATA_VIEW_SAVED_OBJECT_TYPE. All index pattern interfaces were renamed. + */ + +export const INDEX_PATTERN_SAVED_OBJECT_TYPE = DATA_VIEW_SAVED_OBJECT_TYPE; diff --git a/src/plugins/data/common/data_views/data_view.stub.ts b/src/plugins/data_views/common/data_view.stub.ts similarity index 94% rename from src/plugins/data/common/data_views/data_view.stub.ts rename to src/plugins/data_views/common/data_view.stub.ts index a3279434c7a0..2eb6d4f5d781 100644 --- a/src/plugins/data/common/data_views/data_view.stub.ts +++ b/src/plugins/data_views/common/data_view.stub.ts @@ -12,8 +12,8 @@ export { createStubDataView, createStubDataView as createStubIndexPattern, } from './data_views/data_view.stub'; -import { SavedObject } from '../../../../core/types'; -import { DataViewAttributes } from '../types'; +import { SavedObject } from '../../../core/types'; +import { DataViewAttributes } from './types'; export const stubDataView = createStubDataView({ spec: { diff --git a/src/plugins/data/common/data_views/data_views/__snapshots__/data_view.test.ts.snap b/src/plugins/data_views/common/data_views/__snapshots__/data_view.test.ts.snap similarity index 100% rename from src/plugins/data/common/data_views/data_views/__snapshots__/data_view.test.ts.snap rename to src/plugins/data_views/common/data_views/__snapshots__/data_view.test.ts.snap diff --git a/src/plugins/data/common/data_views/data_views/__snapshots__/data_views.test.ts.snap b/src/plugins/data_views/common/data_views/__snapshots__/data_views.test.ts.snap similarity index 100% rename from src/plugins/data/common/data_views/data_views/__snapshots__/data_views.test.ts.snap rename to src/plugins/data_views/common/data_views/__snapshots__/data_views.test.ts.snap diff --git a/src/plugins/data/common/data_views/data_views/_pattern_cache.ts b/src/plugins/data_views/common/data_views/_pattern_cache.ts similarity index 100% rename from src/plugins/data/common/data_views/data_views/_pattern_cache.ts rename to src/plugins/data_views/common/data_views/_pattern_cache.ts diff --git a/src/plugins/data/common/data_views/data_views/data_view.stub.ts b/src/plugins/data_views/common/data_views/data_view.stub.ts similarity index 91% rename from src/plugins/data/common/data_views/data_views/data_view.stub.ts rename to src/plugins/data_views/common/data_views/data_view.stub.ts index 5ff2d077812a..bb7696b0e126 100644 --- a/src/plugins/data/common/data_views/data_views/data_view.stub.ts +++ b/src/plugins/data_views/common/data_views/data_view.stub.ts @@ -8,8 +8,8 @@ import { DataView } from './data_view'; import { DataViewSpec } from '../types'; -import { FieldFormatsStartCommon } from '../../../../field_formats/common'; -import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; +import { FieldFormatsStartCommon } from '../../../field_formats/common'; +import { fieldFormatsMock } from '../../../field_formats/common/mocks'; /** * Create a custom stub index pattern. Use it in your unit tests where an {@link DataView} expected. diff --git a/src/plugins/data/common/data_views/data_views/data_view.test.ts b/src/plugins/data_views/common/data_views/data_view.test.ts similarity index 98% rename from src/plugins/data/common/data_views/data_views/data_view.test.ts rename to src/plugins/data_views/common/data_views/data_view.test.ts index 6aea86a7adae..990b8fa4d5f3 100644 --- a/src/plugins/data/common/data_views/data_views/data_view.test.ts +++ b/src/plugins/data_views/common/data_views/data_view.test.ts @@ -10,12 +10,12 @@ import { map, last } from 'lodash'; import { IndexPattern } from './data_view'; -import { DuplicateField } from '../../../../kibana_utils/common'; +import { DuplicateField } from '../../../kibana_utils/common'; import { IndexPatternField } from '../fields'; -import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; -import { FieldFormat } from '../../../../field_formats/common'; +import { fieldFormatsMock } from '../../../field_formats/common/mocks'; +import { FieldFormat } from '../../../field_formats/common'; import { RuntimeField } from '../types'; import { stubLogstashFields } from '../field.stub'; import { stubbedSavedObjectIndexPattern } from '../data_view.stub'; diff --git a/src/plugins/data/common/data_views/data_views/data_view.ts b/src/plugins/data_views/common/data_views/data_view.ts similarity index 97% rename from src/plugins/data/common/data_views/data_views/data_view.ts rename to src/plugins/data_views/common/data_views/data_view.ts index 18d301d2f9ea..00b96cda32ad 100644 --- a/src/plugins/data/common/data_views/data_views/data_view.ts +++ b/src/plugins/data_views/common/data_views/data_view.ts @@ -9,19 +9,19 @@ /* eslint-disable max-classes-per-file */ import _, { each, reject } from 'lodash'; -import { castEsToKbnFieldTypeName } from '@kbn/field-types'; +import { castEsToKbnFieldTypeName, ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import type { estypes } from '@elastic/elasticsearch'; -import { FieldAttrs, FieldAttrSet, DataViewAttributes } from '../..'; +import { FieldAttrs, FieldAttrSet, DataViewAttributes } from '..'; import type { RuntimeField } from '../types'; -import { DuplicateField } from '../../../../kibana_utils/common'; +import { DuplicateField } from '../../../kibana_utils/common'; -import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; +import { IIndexPattern, IFieldType } from '../../common'; import { DataViewField, IIndexPatternFieldList, fieldList } from '../fields'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; -import { FieldFormatsStartCommon, FieldFormat } from '../../../../field_formats/common'; +import { FieldFormatsStartCommon, FieldFormat } from '../../../field_formats/common'; import { DataViewSpec, TypeMeta, SourceFilter, DataViewFieldMap } from '../types'; -import { SerializedFieldFormat } from '../../../../expressions/common'; +import { SerializedFieldFormat } from '../../../expressions/common'; interface DataViewDeps { spec?: DataViewSpec; diff --git a/src/plugins/data/common/data_views/data_views/data_views.test.ts b/src/plugins/data_views/common/data_views/data_views.test.ts similarity index 99% rename from src/plugins/data/common/data_views/data_views/data_views.test.ts rename to src/plugins/data_views/common/data_views/data_views.test.ts index ef9381f16d93..9a01e52ce48e 100644 --- a/src/plugins/data/common/data_views/data_views/data_views.test.ts +++ b/src/plugins/data_views/common/data_views/data_views.test.ts @@ -8,7 +8,7 @@ import { defaults } from 'lodash'; import { DataViewsService, DataView } from '.'; -import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; +import { fieldFormatsMock } from '../../../field_formats/common/mocks'; import { UiSettingsCommon, SavedObjectsClientCommon, SavedObject } from '../types'; import { stubbedSavedObjectIndexPattern } from '../data_view.stub'; diff --git a/src/plugins/data/common/data_views/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts similarity index 96% rename from src/plugins/data/common/data_views/data_views/data_views.ts rename to src/plugins/data_views/common/data_views/data_views.ts index f9b193d15477..77ce1caaaad8 100644 --- a/src/plugins/data/common/data_views/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { PublicMethodsOf } from '@kbn/utility-types'; import { castEsToKbnFieldTypeName } from '@kbn/field-types'; -import { DATA_VIEW_SAVED_OBJECT_TYPE, SavedObjectsClientCommon } from '../..'; +import { DATA_VIEW_SAVED_OBJECT_TYPE, SavedObjectsClientCommon } from '..'; import { createDataViewCache } from '.'; import type { RuntimeField } from '../types'; @@ -30,9 +30,9 @@ import { DataViewFieldMap, TypeMeta, } from '../types'; -import { FieldFormatsStartCommon, FORMATS_UI_SETTINGS } from '../../../../field_formats/common/'; -import { UI_SETTINGS, SavedObject } from '../../../common'; -import { SavedObjectNotFound } from '../../../../kibana_utils/common'; +import { FieldFormatsStartCommon, FORMATS_UI_SETTINGS } from '../../../field_formats/common/'; +import { META_FIELDS, SavedObject } from '../../common'; +import { SavedObjectNotFound } from '../../../kibana_utils/common'; import { DataViewMissingIndices } from '../lib'; import { findByTitle } from '../utils'; import { DuplicateDataViewError } from '../errors'; @@ -244,7 +244,7 @@ export class DataViewsService { * @returns FieldSpec[] */ getFieldsForWildcard = async (options: GetFieldsOptions) => { - const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); + const metaFields = await this.config.get(META_FIELDS); return this.apiClient.getFieldsForWildcard({ pattern: options.pattern, metaFields, @@ -290,7 +290,7 @@ export class DataViewsService { } this.onError(err, { - title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { + title: i18n.translate('dataViews.fetchFieldErrorTitle', { defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', values: { id: indexPattern.id, title: indexPattern.title }, }), @@ -336,7 +336,7 @@ export class DataViewsService { } this.onError(err, { - title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { + title: i18n.translate('dataViews.fetchFieldErrorTitle', { defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', values: { id, title }, }), @@ -445,7 +445,7 @@ export class DataViewsService { spec.title as string, { pattern: title as string, - metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), + metaFields: await this.config.get(META_FIELDS), type, rollupIndex: typeMeta?.params?.rollup_index, allowNoIndex: spec.allowNoIndex, @@ -478,7 +478,7 @@ export class DataViewsService { }); } else { this.onError(err, { - title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { + title: i18n.translate('dataViews.fetchFieldErrorTitle', { defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', values: { id: savedObject.id, title }, }), @@ -520,7 +520,7 @@ export class DataViewsService { */ async create(spec: DataViewSpec, skipFetchFields = false): Promise { const shortDotsEnable = await this.config.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE); - const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); + const metaFields = await this.config.get(META_FIELDS); const indexPattern = new DataView({ spec, @@ -648,7 +648,7 @@ export class DataViewsService { if (ignoreErrors) { return; } - const title = i18n.translate('data.indexPatterns.unableWriteLabel', { + const title = i18n.translate('dataViews.unableWriteLabel', { defaultMessage: 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.', }); diff --git a/src/plugins/data/common/data_views/data_views/ensure_default_data_view.ts b/src/plugins/data_views/common/data_views/ensure_default_data_view.ts similarity index 100% rename from src/plugins/data/common/data_views/data_views/ensure_default_data_view.ts rename to src/plugins/data_views/common/data_views/ensure_default_data_view.ts diff --git a/src/plugins/data/common/data_views/data_views/flatten_hit.test.ts b/src/plugins/data_views/common/data_views/flatten_hit.test.ts similarity index 96% rename from src/plugins/data/common/data_views/data_views/flatten_hit.test.ts rename to src/plugins/data_views/common/data_views/flatten_hit.test.ts index 73232a65b6b7..0946f30b85e3 100644 --- a/src/plugins/data/common/data_views/data_views/flatten_hit.test.ts +++ b/src/plugins/data_views/common/data_views/flatten_hit.test.ts @@ -8,7 +8,7 @@ import { DataView } from './data_view'; -import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; +import { fieldFormatsMock } from '../../../field_formats/common/mocks'; import { flattenHitWrapper } from './flatten_hit'; import { stubbedSavedObjectIndexPattern } from '../data_view.stub'; diff --git a/src/plugins/data/common/data_views/data_views/flatten_hit.ts b/src/plugins/data_views/common/data_views/flatten_hit.ts similarity index 100% rename from src/plugins/data/common/data_views/data_views/flatten_hit.ts rename to src/plugins/data_views/common/data_views/flatten_hit.ts diff --git a/src/plugins/data/common/data_views/data_views/format_hit.ts b/src/plugins/data_views/common/data_views/format_hit.ts similarity index 97% rename from src/plugins/data/common/data_views/data_views/format_hit.ts rename to src/plugins/data_views/common/data_views/format_hit.ts index 39f7fef564eb..30daf7768c4d 100644 --- a/src/plugins/data/common/data_views/data_views/format_hit.ts +++ b/src/plugins/data_views/common/data_views/format_hit.ts @@ -8,7 +8,7 @@ import _ from 'lodash'; import { DataView } from './data_view'; -import { FieldFormatsContentType } from '../../../../field_formats/common'; +import { FieldFormatsContentType } from '../../../field_formats/common'; const formattedCache = new WeakMap(); const partialFormattedCache = new WeakMap(); diff --git a/src/plugins/data/common/data_views/data_views/index.ts b/src/plugins/data_views/common/data_views/index.ts similarity index 100% rename from src/plugins/data/common/data_views/data_views/index.ts rename to src/plugins/data_views/common/data_views/index.ts diff --git a/src/plugins/data/common/data_views/errors/data_view_saved_object_conflict.ts b/src/plugins/data_views/common/errors/data_view_saved_object_conflict.ts similarity index 100% rename from src/plugins/data/common/data_views/errors/data_view_saved_object_conflict.ts rename to src/plugins/data_views/common/errors/data_view_saved_object_conflict.ts diff --git a/src/plugins/data/common/data_views/errors/duplicate_index_pattern.ts b/src/plugins/data_views/common/errors/duplicate_index_pattern.ts similarity index 100% rename from src/plugins/data/common/data_views/errors/duplicate_index_pattern.ts rename to src/plugins/data_views/common/errors/duplicate_index_pattern.ts diff --git a/src/plugins/data/common/data_views/errors/index.ts b/src/plugins/data_views/common/errors/index.ts similarity index 100% rename from src/plugins/data/common/data_views/errors/index.ts rename to src/plugins/data_views/common/errors/index.ts diff --git a/src/plugins/data/common/data_views/expressions/index.ts b/src/plugins/data_views/common/expressions/index.ts similarity index 100% rename from src/plugins/data/common/data_views/expressions/index.ts rename to src/plugins/data_views/common/expressions/index.ts diff --git a/src/plugins/data/common/data_views/expressions/load_index_pattern.ts b/src/plugins/data_views/common/expressions/load_index_pattern.ts similarity index 90% rename from src/plugins/data/common/data_views/expressions/load_index_pattern.ts rename to src/plugins/data_views/common/expressions/load_index_pattern.ts index dd47a9fc0dfb..ca0c1090ceea 100644 --- a/src/plugins/data/common/data_views/expressions/load_index_pattern.ts +++ b/src/plugins/data_views/common/expressions/load_index_pattern.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { DataViewsContract } from '../data_views'; import { DataViewSpec } from '..'; -import { SavedObjectReference } from '../../../../../core/types'; +import { SavedObjectReference } from '../../../../core/types'; const name = 'indexPatternLoad'; const type = 'index_pattern'; @@ -46,14 +46,14 @@ export const getIndexPatternLoadMeta = (): Omit< name, type, inputTypes: ['null'], - help: i18n.translate('data.functions.indexPatternLoad.help', { + help: i18n.translate('dataViews.indexPatternLoad.help', { defaultMessage: 'Loads an index pattern', }), args: { id: { types: ['string'], required: true, - help: i18n.translate('data.functions.indexPatternLoad.id.help', { + help: i18n.translate('dataViews.functions.indexPatternLoad.id.help', { defaultMessage: 'index pattern id to load', }), }, diff --git a/src/plugins/data/common/data_views/field.stub.ts b/src/plugins/data_views/common/field.stub.ts similarity index 100% rename from src/plugins/data/common/data_views/field.stub.ts rename to src/plugins/data_views/common/field.stub.ts diff --git a/src/plugins/data/common/data_views/fields/__snapshots__/data_view_field.test.ts.snap b/src/plugins/data_views/common/fields/__snapshots__/data_view_field.test.ts.snap similarity index 100% rename from src/plugins/data/common/data_views/fields/__snapshots__/data_view_field.test.ts.snap rename to src/plugins/data_views/common/fields/__snapshots__/data_view_field.test.ts.snap diff --git a/src/plugins/data/common/data_views/fields/data_view_field.test.ts b/src/plugins/data_views/common/fields/data_view_field.test.ts similarity index 97% rename from src/plugins/data/common/data_views/fields/data_view_field.test.ts rename to src/plugins/data_views/common/fields/data_view_field.test.ts index 9107036c15c1..9c611354683c 100644 --- a/src/plugins/data/common/data_views/fields/data_view_field.test.ts +++ b/src/plugins/data_views/common/fields/data_view_field.test.ts @@ -8,9 +8,9 @@ import { IndexPatternField } from './data_view_field'; import { IndexPattern } from '..'; -import { KBN_FIELD_TYPES } from '../../../common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { FieldSpec, RuntimeField } from '../types'; -import { FieldFormat } from '../../../../field_formats/common'; +import { FieldFormat } from '../../../field_formats/common'; describe('Field', function () { function flatten(obj: Record) { diff --git a/src/plugins/data/common/data_views/fields/data_view_field.ts b/src/plugins/data_views/common/fields/data_view_field.ts similarity index 97% rename from src/plugins/data/common/data_views/fields/data_view_field.ts rename to src/plugins/data_views/common/fields/data_view_field.ts index fae0e14b95c0..3ad92a3a7e53 100644 --- a/src/plugins/data/common/data_views/fields/data_view_field.ts +++ b/src/plugins/data_views/common/fields/data_view_field.ts @@ -9,11 +9,11 @@ /* eslint-disable max-classes-per-file */ import { KbnFieldType, getKbnFieldType, castEsToKbnFieldTypeName } from '@kbn/field-types'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import type { RuntimeField } from '../types'; -import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import type { IFieldType } from './types'; -import { FieldSpec, DataView } from '../..'; -import { shortenDottedString } from '../../utils'; +import { FieldSpec, DataView } from '..'; +import { shortenDottedString } from './utils'; /** @public */ export class DataViewField implements IFieldType { diff --git a/src/plugins/data/common/data_views/fields/field_list.ts b/src/plugins/data_views/common/fields/field_list.ts similarity index 100% rename from src/plugins/data/common/data_views/fields/field_list.ts rename to src/plugins/data_views/common/fields/field_list.ts diff --git a/src/plugins/data/common/data_views/fields/fields.mocks.ts b/src/plugins/data_views/common/fields/fields.mocks.ts similarity index 100% rename from src/plugins/data/common/data_views/fields/fields.mocks.ts rename to src/plugins/data_views/common/fields/fields.mocks.ts diff --git a/src/plugins/data/common/data_views/fields/index.ts b/src/plugins/data_views/common/fields/index.ts similarity index 100% rename from src/plugins/data/common/data_views/fields/index.ts rename to src/plugins/data_views/common/fields/index.ts diff --git a/src/plugins/data/common/data_views/fields/types.ts b/src/plugins/data_views/common/fields/types.ts similarity index 95% rename from src/plugins/data/common/data_views/fields/types.ts rename to src/plugins/data_views/common/fields/types.ts index 2c5934a8e7b3..2bd1cf583454 100644 --- a/src/plugins/data/common/data_views/fields/types.ts +++ b/src/plugins/data_views/common/fields/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { DataViewFieldBase } from '@kbn/es-query'; -import { FieldSpec, DataView } from '../..'; +import { FieldSpec, DataView } from '..'; /** * @deprecated Use {@link IndexPatternField} diff --git a/src/plugins/data/common/utils/shorten_dotted_string.test.ts b/src/plugins/data_views/common/fields/utils.test.ts similarity index 92% rename from src/plugins/data/common/utils/shorten_dotted_string.test.ts rename to src/plugins/data_views/common/fields/utils.test.ts index 33a44925982e..0f2ff280eb61 100644 --- a/src/plugins/data/common/utils/shorten_dotted_string.test.ts +++ b/src/plugins/data_views/common/fields/utils.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { shortenDottedString } from './shorten_dotted_string'; +import { shortenDottedString } from './utils'; describe('shortenDottedString', () => { test('should convert a dot.notated.string into a short string', () => { diff --git a/src/plugins/data/common/data_views/fields/utils.ts b/src/plugins/data_views/common/fields/utils.ts similarity index 75% rename from src/plugins/data/common/data_views/fields/utils.ts rename to src/plugins/data_views/common/fields/utils.ts index 9e05bebc746f..8344a32d7855 100644 --- a/src/plugins/data/common/data_views/fields/utils.ts +++ b/src/plugins/data_views/common/fields/utils.ts @@ -22,3 +22,15 @@ export function isFilterable(field: IFieldType): boolean { export function isNestedField(field: IFieldType): boolean { return !!field.subType?.nested; } + +const DOT_PREFIX_RE = /(.).+?\./g; + +/** + * Convert a dot.notated.string into a short + * version (d.n.string) + * + * @return {any} + */ +export function shortenDottedString(input: any) { + return typeof input !== 'string' ? input : input.replace(DOT_PREFIX_RE, '$1.'); +} diff --git a/src/plugins/data_views/common/index.ts b/src/plugins/data_views/common/index.ts new file mode 100644 index 000000000000..e8b36ab3e8fc --- /dev/null +++ b/src/plugins/data_views/common/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + RUNTIME_FIELD_TYPES, + FLEET_ASSETS_TO_IGNORE, + META_FIELDS, + DATA_VIEW_SAVED_OBJECT_TYPE, + INDEX_PATTERN_SAVED_OBJECT_TYPE, +} from './constants'; +export type { IFieldType, IIndexPatternFieldList } from './fields'; +export { isFilterable, isNestedField, fieldList, DataViewField, IndexPatternField } from './fields'; +export type { + FieldFormatMap, + RuntimeType, + RuntimeField, + IIndexPattern, + DataViewAttributes, + IndexPatternAttributes, + FieldAttrs, + FieldAttrSet, + OnNotification, + OnError, + UiSettingsCommon, + SavedObjectsClientCommonFindArgs, + SavedObjectsClientCommon, + GetFieldsOptions, + GetFieldsOptionsTimePattern, + IDataViewsApiClient, + IIndexPatternsApiClient, + SavedObject, + AggregationRestrictions, + TypeMeta, + FieldSpecConflictDescriptions, + FieldSpecExportFmt, + FieldSpec, + DataViewFieldMap, + IndexPatternFieldMap, + DataViewSpec, + IndexPatternSpec, + SourceFilter, +} from './types'; +export { DataViewType, IndexPatternType } from './types'; +export { + IndexPatternsService, + IndexPatternsContract, + DataViewsService, + DataViewsContract, +} from './data_views'; +export { IndexPattern, IndexPatternListItem, DataView, DataViewListItem } from './data_views'; +export { DuplicateDataViewError, DataViewSavedObjectConflictError } from './errors'; +export type { + IndexPatternExpressionType, + IndexPatternLoadStartDependencies, + IndexPatternLoadExpressionFunctionDefinition, +} from './expressions'; +export { getIndexPatternLoadMeta } from './expressions'; diff --git a/src/plugins/data/common/data_views/lib/errors.ts b/src/plugins/data_views/common/lib/errors.ts similarity index 93% rename from src/plugins/data/common/data_views/lib/errors.ts rename to src/plugins/data_views/common/lib/errors.ts index 83cc7ea56d02..f8422a6e5dd0 100644 --- a/src/plugins/data/common/data_views/lib/errors.ts +++ b/src/plugins/data_views/common/lib/errors.ts @@ -8,7 +8,7 @@ /* eslint-disable */ -import { KbnError } from '../../../../kibana_utils/common/'; +import { KbnError } from '../../../kibana_utils/common'; /** * Tried to call a method that relies on SearchSource having an indexPattern assigned diff --git a/src/plugins/data/common/data_views/lib/get_title.ts b/src/plugins/data_views/common/lib/get_title.ts similarity index 85% rename from src/plugins/data/common/data_views/lib/get_title.ts rename to src/plugins/data_views/common/lib/get_title.ts index 94185eae4689..69471583f139 100644 --- a/src/plugins/data/common/data_views/lib/get_title.ts +++ b/src/plugins/data_views/common/lib/get_title.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { SavedObjectsClientContract } from '../../../../../core/public'; -import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../constants'; +import { SavedObjectsClientContract } from '../../../../core/public'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../constants'; import { DataViewAttributes } from '../types'; export async function getTitle( diff --git a/src/plugins/data/common/data_views/lib/index.ts b/src/plugins/data_views/common/lib/index.ts similarity index 100% rename from src/plugins/data/common/data_views/lib/index.ts rename to src/plugins/data_views/common/lib/index.ts diff --git a/src/plugins/data/common/data_views/lib/types.ts b/src/plugins/data_views/common/lib/types.ts similarity index 100% rename from src/plugins/data/common/data_views/lib/types.ts rename to src/plugins/data_views/common/lib/types.ts diff --git a/src/plugins/data/common/data_views/lib/validate_data_view.test.ts b/src/plugins/data_views/common/lib/validate_data_view.test.ts similarity index 100% rename from src/plugins/data/common/data_views/lib/validate_data_view.test.ts rename to src/plugins/data_views/common/lib/validate_data_view.test.ts diff --git a/src/plugins/data/common/data_views/lib/validate_data_view.ts b/src/plugins/data_views/common/lib/validate_data_view.ts similarity index 100% rename from src/plugins/data/common/data_views/lib/validate_data_view.ts rename to src/plugins/data_views/common/lib/validate_data_view.ts diff --git a/src/plugins/data/common/data_views/mocks.ts b/src/plugins/data_views/common/mocks.ts similarity index 100% rename from src/plugins/data/common/data_views/mocks.ts rename to src/plugins/data_views/common/mocks.ts diff --git a/src/core/server/legacy/index.ts b/src/plugins/data_views/common/stubs.ts similarity index 74% rename from src/core/server/legacy/index.ts rename to src/plugins/data_views/common/stubs.ts index 39ffef501a9e..c6895da9bfb3 100644 --- a/src/core/server/legacy/index.ts +++ b/src/plugins/data_views/common/stubs.ts @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -/** @internal */ -export type { ILegacyService } from './legacy_service'; -export { LegacyService } from './legacy_service'; +export * from './field.stub'; +export * from './data_views/data_view.stub'; diff --git a/src/plugins/data/common/data_views/types.ts b/src/plugins/data_views/common/types.ts similarity index 96% rename from src/plugins/data/common/data_views/types.ts rename to src/plugins/data_views/common/types.ts index 85fe98fbcfeb..2b184bc1ef2a 100644 --- a/src/plugins/data/common/data_views/types.ts +++ b/src/plugins/data_views/common/types.ts @@ -10,11 +10,12 @@ import type { DataViewFieldBase, IFieldSubType, DataViewBase } from '@kbn/es-que import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications'; // eslint-disable-next-line import type { SavedObject } from 'src/core/server'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { IFieldType } from './fields'; import { RUNTIME_FIELD_TYPES } from './constants'; -import { SerializedFieldFormat } from '../../../expressions/common'; -import { KBN_FIELD_TYPES, DataViewField } from '..'; -import { FieldFormat } from '../../../field_formats/common'; +import { SerializedFieldFormat } from '../../expressions/common'; +import { DataViewField } from './fields'; +import { FieldFormat } from '../../field_formats/common'; export type FieldFormatMap = Record; diff --git a/src/plugins/data/common/data_views/utils.test.ts b/src/plugins/data_views/common/utils.test.ts similarity index 100% rename from src/plugins/data/common/data_views/utils.test.ts rename to src/plugins/data_views/common/utils.test.ts diff --git a/src/plugins/data/common/data_views/utils.ts b/src/plugins/data_views/common/utils.ts similarity index 89% rename from src/plugins/data/common/data_views/utils.ts rename to src/plugins/data_views/common/utils.ts index 2d36ab6c7222..77e9bd76b869 100644 --- a/src/plugins/data/common/data_views/utils.ts +++ b/src/plugins/data_views/common/utils.ts @@ -7,9 +7,9 @@ */ import type { IndexPatternSavedObjectAttrs } from './data_views'; -import type { SavedObjectsClientCommon } from '../types'; +import type { SavedObjectsClientCommon } from './types'; -import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../constants'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from './constants'; /** * Returns an object matching a given title diff --git a/src/plugins/data_views/jest.config.js b/src/plugins/data_views/jest.config.js new file mode 100644 index 000000000000..4c1f06783563 --- /dev/null +++ b/src/plugins/data_views/jest.config.js @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/data_views'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/data_views', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/data_views/{common,public,server}/**/*.{ts,tsx}'], +}; diff --git a/src/plugins/data_views/kibana.json b/src/plugins/data_views/kibana.json new file mode 100644 index 000000000000..27bf536ef804 --- /dev/null +++ b/src/plugins/data_views/kibana.json @@ -0,0 +1,15 @@ +{ + "id": "dataViews", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["fieldFormats","expressions"], + "optionalPlugins": ["usageCollection"], + "extraPublicDirs": ["common"], + "requiredBundles": ["kibanaUtils","kibanaReact"], + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "description": "Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters." +} diff --git a/src/plugins/data/public/data_views/data_views/data_view.stub.ts b/src/plugins/data_views/public/data_views/data_view.stub.ts similarity index 84% rename from src/plugins/data/public/data_views/data_views/data_view.stub.ts rename to src/plugins/data_views/public/data_views/data_view.stub.ts index b3d8448064c6..f37a8a78b234 100644 --- a/src/plugins/data/public/data_views/data_views/data_view.stub.ts +++ b/src/plugins/data_views/public/data_views/data_view.stub.ts @@ -7,11 +7,11 @@ */ import { CoreSetup } from 'kibana/public'; -import { FieldFormatsStartCommon } from '../../../../field_formats/common'; -import { getFieldFormatsRegistry } from '../../../../field_formats/public/mocks'; -import * as commonStubs from '../../../common/stubs'; -import { DataView, DataViewSpec } from '../../../common'; -import { coreMock } from '../../../../../core/public/mocks'; +import { FieldFormatsStartCommon } from '../../../field_formats/common'; +import { getFieldFormatsRegistry } from '../../../field_formats/public/mocks'; +import * as commonStubs from '../../common/stubs'; +import { DataView, DataViewSpec } from '../../common'; +import { coreMock } from '../../../../core/public/mocks'; /** * Create a custom stub index pattern. Use it in your unit tests where an {@link DataView} expected. * @param spec - Serialized index pattern object diff --git a/src/plugins/data/public/data_views/data_views/data_views_api_client.test.mock.ts b/src/plugins/data_views/public/data_views/data_views_api_client.test.mock.ts similarity index 86% rename from src/plugins/data/public/data_views/data_views/data_views_api_client.test.mock.ts rename to src/plugins/data_views/public/data_views/data_views_api_client.test.mock.ts index c53ca7ad89aa..2fd17b98f749 100644 --- a/src/plugins/data/public/data_views/data_views/data_views_api_client.test.mock.ts +++ b/src/plugins/data_views/public/data_views/data_views_api_client.test.mock.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { setup } from '../../../../../core/test_helpers/http_test_setup'; +import { setup } from '../../../../core/test_helpers/http_test_setup'; export const { http } = setup((injectedMetadata) => { injectedMetadata.getBasePath.mockReturnValue('/hola/daro/'); diff --git a/src/plugins/data/public/data_views/data_views/data_views_api_client.test.ts b/src/plugins/data_views/public/data_views/data_views_api_client.test.ts similarity index 100% rename from src/plugins/data/public/data_views/data_views/data_views_api_client.test.ts rename to src/plugins/data_views/public/data_views/data_views_api_client.test.ts diff --git a/src/plugins/data/public/data_views/data_views/data_views_api_client.ts b/src/plugins/data_views/public/data_views/data_views_api_client.ts similarity index 90% rename from src/plugins/data/public/data_views/data_views/data_views_api_client.ts rename to src/plugins/data_views/public/data_views/data_views_api_client.ts index d11ec7cfa003..d4da9a55c25d 100644 --- a/src/plugins/data/public/data_views/data_views/data_views_api_client.ts +++ b/src/plugins/data_views/public/data_views/data_views_api_client.ts @@ -7,12 +7,8 @@ */ import { HttpSetup } from 'src/core/public'; -import { DataViewMissingIndices } from '../../../common/data_views/lib'; -import { - GetFieldsOptions, - IDataViewsApiClient, - GetFieldsOptionsTimePattern, -} from '../../../common/data_views/types'; +import { DataViewMissingIndices } from '../../common/lib'; +import { GetFieldsOptions, IDataViewsApiClient, GetFieldsOptionsTimePattern } from '../../common'; const API_BASE_URL: string = `/api/index_patterns/`; diff --git a/src/plugins/data/public/data_views/data_views/index.ts b/src/plugins/data_views/public/data_views/index.ts similarity index 88% rename from src/plugins/data/public/data_views/data_views/index.ts rename to src/plugins/data_views/public/data_views/index.ts index e0d18d47f39d..e476d62774f1 100644 --- a/src/plugins/data/public/data_views/data_views/index.ts +++ b/src/plugins/data_views/public/data_views/index.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export * from '../../../common/data_views/data_views'; +export * from '../../common/data_views'; export * from './redirect_no_index_pattern'; export * from './data_views_api_client'; diff --git a/src/plugins/data/public/data_views/data_views/redirect_no_index_pattern.tsx b/src/plugins/data_views/public/data_views/redirect_no_index_pattern.tsx similarity index 84% rename from src/plugins/data/public/data_views/data_views/redirect_no_index_pattern.tsx rename to src/plugins/data_views/public/data_views/redirect_no_index_pattern.tsx index 88e18060c4d1..456af90a3c6d 100644 --- a/src/plugins/data/public/data_views/data_views/redirect_no_index_pattern.tsx +++ b/src/plugins/data_views/public/data_views/redirect_no_index_pattern.tsx @@ -10,7 +10,7 @@ import { EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { CoreStart } from 'kibana/public'; -import { toMountPoint } from '../../../../kibana_react/public'; +import { toMountPoint } from '../../../kibana_react/public'; let bannerId: string; @@ -29,13 +29,10 @@ export const onRedirectNoIndexPattern = clearTimeout(timeoutId); } - const bannerMessage = i18n.translate( - 'data.indexPatterns.ensureDefaultIndexPattern.bannerLabel', - { - defaultMessage: - 'To visualize and explore data in Kibana, you must create an index pattern to retrieve data from Elasticsearch.', - } - ); + const bannerMessage = i18n.translate('dataViews.ensureDefaultIndexPattern.bannerLabel', { + defaultMessage: + 'To visualize and explore data in Kibana, you must create an index pattern to retrieve data from Elasticsearch.', + }); // Avoid being hostile to new users who don't have an index pattern setup yet // give them a friendly info message instead of a terse error message diff --git a/src/plugins/data/public/data_views/expressions/index.ts b/src/plugins/data_views/public/expressions/index.ts similarity index 100% rename from src/plugins/data/public/data_views/expressions/index.ts rename to src/plugins/data_views/public/expressions/index.ts diff --git a/src/plugins/data/public/data_views/expressions/load_index_pattern.test.ts b/src/plugins/data_views/public/expressions/load_index_pattern.test.ts similarity index 92% rename from src/plugins/data/public/data_views/expressions/load_index_pattern.test.ts rename to src/plugins/data_views/public/expressions/load_index_pattern.test.ts index befa78c39898..02a530712e80 100644 --- a/src/plugins/data/public/data_views/expressions/load_index_pattern.test.ts +++ b/src/plugins/data_views/public/expressions/load_index_pattern.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPatternLoadStartDependencies } from '../../../common/data_views/expressions'; +import { IndexPatternLoadStartDependencies } from '../../common/expressions'; import { getFunctionDefinition } from './load_index_pattern'; describe('indexPattern expression function', () => { diff --git a/src/plugins/data/public/data_views/expressions/load_index_pattern.ts b/src/plugins/data_views/public/expressions/load_index_pattern.ts similarity index 88% rename from src/plugins/data/public/data_views/expressions/load_index_pattern.ts rename to src/plugins/data_views/public/expressions/load_index_pattern.ts index 979861c7da38..76119f3e50a4 100644 --- a/src/plugins/data/public/data_views/expressions/load_index_pattern.ts +++ b/src/plugins/data_views/public/expressions/load_index_pattern.ts @@ -11,8 +11,8 @@ import { getIndexPatternLoadMeta, IndexPatternLoadExpressionFunctionDefinition, IndexPatternLoadStartDependencies, -} from '../../../common/data_views/expressions'; -import { DataPublicPluginStart, DataStartDependencies } from '../../types'; +} from '../../common/expressions'; +import { DataViewsPublicPluginStart, DataViewsPublicStartDependencies } from '../types'; /** * Returns the expression function definition. Any stateful dependencies are accessed @@ -60,11 +60,14 @@ export function getFunctionDefinition({ export function getIndexPatternLoad({ getStartServices, }: { - getStartServices: StartServicesAccessor; + getStartServices: StartServicesAccessor< + DataViewsPublicStartDependencies, + DataViewsPublicPluginStart + >; }) { return getFunctionDefinition({ getStartDependencies: async () => { - const [, , { indexPatterns }] = await getStartServices(); + const [, , indexPatterns] = await getStartServices(); return { indexPatterns }; }, }); diff --git a/src/plugins/data_views/public/index.ts b/src/plugins/data_views/public/index.ts new file mode 100644 index 000000000000..572806df11fa --- /dev/null +++ b/src/plugins/data_views/public/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + ILLEGAL_CHARACTERS_KEY, + CONTAINS_SPACES_KEY, + ILLEGAL_CHARACTERS_VISIBLE, + ILLEGAL_CHARACTERS, + validateDataView, +} from '../common/lib'; +export { flattenHitWrapper, formatHitProvider, onRedirectNoIndexPattern } from './data_views'; + +export { IndexPatternField, IIndexPatternFieldList, TypeMeta } from '../common'; + +export { + IndexPatternsService, + IndexPatternsContract, + IndexPattern, + DataViewsApiClient, + DataViewsService, + DataViewsContract, + DataView, +} from './data_views'; +export { UiSettingsPublicToCommon } from './ui_settings_wrapper'; +export { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper'; + +/* + * Plugin setup + */ + +import { DataViewsPublicPlugin } from './plugin'; + +export function plugin() { + return new DataViewsPublicPlugin(); +} + +export type { DataViewsPublicPluginSetup, DataViewsPublicPluginStart } from './types'; + +// Export plugin after all other imports +export type { DataViewsPublicPlugin as DataPlugin }; diff --git a/src/plugins/data_views/public/plugin.ts b/src/plugins/data_views/public/plugin.ts new file mode 100644 index 000000000000..58f66623b64a --- /dev/null +++ b/src/plugins/data_views/public/plugin.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { getIndexPatternLoad } from './expressions'; +import { + DataViewsPublicPluginSetup, + DataViewsPublicPluginStart, + DataViewsPublicSetupDependencies, + DataViewsPublicStartDependencies, +} from './types'; + +import { + DataViewsService, + onRedirectNoIndexPattern, + DataViewsApiClient, + UiSettingsPublicToCommon, + SavedObjectsClientPublicToCommon, +} from '.'; + +export class DataViewsPublicPlugin + implements + Plugin< + DataViewsPublicPluginSetup, + DataViewsPublicPluginStart, + DataViewsPublicSetupDependencies, + DataViewsPublicStartDependencies + > +{ + public setup( + core: CoreSetup, + { expressions }: DataViewsPublicSetupDependencies + ): DataViewsPublicPluginSetup { + expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices })); + + return {}; + } + + public start( + core: CoreStart, + { fieldFormats }: DataViewsPublicStartDependencies + ): DataViewsPublicPluginStart { + const { uiSettings, http, notifications, savedObjects, overlays, application } = core; + + return new DataViewsService({ + uiSettings: new UiSettingsPublicToCommon(uiSettings), + savedObjectsClient: new SavedObjectsClientPublicToCommon(savedObjects.client), + apiClient: new DataViewsApiClient(http), + fieldFormats, + onNotification: (toastInputFields) => { + notifications.toasts.add(toastInputFields); + }, + onError: notifications.toasts.addError.bind(notifications.toasts), + onRedirectNoIndexPattern: onRedirectNoIndexPattern( + application.capabilities, + application.navigateToApp, + overlays + ), + }); + } + + public stop() {} +} diff --git a/src/plugins/data/public/data_views/saved_objects_client_wrapper.test.ts b/src/plugins/data_views/public/saved_objects_client_wrapper.test.ts similarity index 96% rename from src/plugins/data/public/data_views/saved_objects_client_wrapper.test.ts rename to src/plugins/data_views/public/saved_objects_client_wrapper.test.ts index 221a18ac7fab..124a66eba57f 100644 --- a/src/plugins/data/public/data_views/saved_objects_client_wrapper.test.ts +++ b/src/plugins/data_views/public/saved_objects_client_wrapper.test.ts @@ -9,7 +9,7 @@ import { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper'; import { savedObjectsServiceMock } from 'src/core/public/mocks'; -import { DataViewSavedObjectConflictError } from '../../common/data_views'; +import { DataViewSavedObjectConflictError } from '../common'; describe('SavedObjectsClientPublicToCommon', () => { const soClient = savedObjectsServiceMock.createStartContract().client; diff --git a/src/plugins/data/public/data_views/saved_objects_client_wrapper.ts b/src/plugins/data_views/public/saved_objects_client_wrapper.ts similarity index 98% rename from src/plugins/data/public/data_views/saved_objects_client_wrapper.ts rename to src/plugins/data_views/public/saved_objects_client_wrapper.ts index 1db4e3b1ccd2..beaae6ac3fc2 100644 --- a/src/plugins/data/public/data_views/saved_objects_client_wrapper.ts +++ b/src/plugins/data_views/public/saved_objects_client_wrapper.ts @@ -13,7 +13,7 @@ import { SavedObjectsClientCommonFindArgs, SavedObject, DataViewSavedObjectConflictError, -} from '../../common/data_views'; +} from '../common'; type SOClient = Pick; diff --git a/src/plugins/data_views/public/types.ts b/src/plugins/data_views/public/types.ts new file mode 100644 index 000000000000..20b1cbaf090f --- /dev/null +++ b/src/plugins/data_views/public/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExpressionsSetup } from 'src/plugins/expressions/public'; +import { FieldFormatsSetup, FieldFormatsStart } from 'src/plugins/field_formats/public'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { DataViewsService } from './data_views'; + +export interface DataViewsPublicSetupDependencies { + expressions: ExpressionsSetup; + fieldFormats: FieldFormatsSetup; +} + +export interface DataViewsPublicStartDependencies { + fieldFormats: FieldFormatsStart; +} + +/** + * Data plugin public Setup contract + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DataViewsPublicPluginSetup {} + +/** + * Data plugin public Start contract + */ +export type DataViewsPublicPluginStart = PublicMethodsOf; diff --git a/src/plugins/data/public/data_views/ui_settings_wrapper.ts b/src/plugins/data_views/public/ui_settings_wrapper.ts similarity index 95% rename from src/plugins/data/public/data_views/ui_settings_wrapper.ts rename to src/plugins/data_views/public/ui_settings_wrapper.ts index f8ae317391fa..91806867b673 100644 --- a/src/plugins/data/public/data_views/ui_settings_wrapper.ts +++ b/src/plugins/data_views/public/ui_settings_wrapper.ts @@ -7,7 +7,7 @@ */ import { IUiSettingsClient, PublicUiSettingsParams, UserProvidedValues } from 'src/core/public'; -import { UiSettingsCommon } from '../../common'; +import { UiSettingsCommon } from '../common'; export class UiSettingsPublicToCommon implements UiSettingsCommon { private uiSettings: IUiSettingsClient; diff --git a/src/plugins/data/server/data_views/capabilities_provider.ts b/src/plugins/data_views/server/capabilities_provider.ts similarity index 100% rename from src/plugins/data/server/data_views/capabilities_provider.ts rename to src/plugins/data_views/server/capabilities_provider.ts diff --git a/src/plugins/data_views/server/data_views_service_factory.ts b/src/plugins/data_views/server/data_views_service_factory.ts new file mode 100644 index 000000000000..2f720cd7388f --- /dev/null +++ b/src/plugins/data_views/server/data_views_service_factory.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + Logger, + SavedObjectsClientContract, + ElasticsearchClient, + UiSettingsServiceStart, +} from 'kibana/server'; +import { DataViewsService } from '../common'; +import { FieldFormatsStart } from '../../field_formats/server'; +import { UiSettingsServerToCommon } from './ui_settings_wrapper'; +import { IndexPatternsApiServer } from './index_patterns_api_client'; +import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper'; + +export const dataViewsServiceFactory = + ({ + logger, + uiSettings, + fieldFormats, + }: { + logger: Logger; + uiSettings: UiSettingsServiceStart; + fieldFormats: FieldFormatsStart; + }) => + async ( + savedObjectsClient: SavedObjectsClientContract, + elasticsearchClient: ElasticsearchClient + ) => { + const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); + + return new DataViewsService({ + uiSettings: new UiSettingsServerToCommon(uiSettingsClient), + savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), + apiClient: new IndexPatternsApiServer(elasticsearchClient, savedObjectsClient), + fieldFormats: formats, + onError: (error) => { + logger.error(error); + }, + onNotification: ({ title, text }) => { + logger.warn(`${title}${text ? ` : ${text}` : ''}`); + }, + }); + }; diff --git a/src/plugins/data/server/data_views/deprecations/index.ts b/src/plugins/data_views/server/deprecations/index.ts similarity index 100% rename from src/plugins/data/server/data_views/deprecations/index.ts rename to src/plugins/data_views/server/deprecations/index.ts diff --git a/src/plugins/data/server/data_views/deprecations/scripted_fields.test.ts b/src/plugins/data_views/server/deprecations/scripted_fields.test.ts similarity index 100% rename from src/plugins/data/server/data_views/deprecations/scripted_fields.test.ts rename to src/plugins/data_views/server/deprecations/scripted_fields.test.ts diff --git a/src/plugins/data/server/data_views/deprecations/scripted_fields.ts b/src/plugins/data_views/server/deprecations/scripted_fields.ts similarity index 89% rename from src/plugins/data/server/data_views/deprecations/scripted_fields.ts rename to src/plugins/data_views/server/deprecations/scripted_fields.ts index 65cb96219680..9ee2d64e25cb 100644 --- a/src/plugins/data/server/data_views/deprecations/scripted_fields.ts +++ b/src/plugins/data_views/server/deprecations/scripted_fields.ts @@ -13,7 +13,7 @@ import { RegisterDeprecationsConfig, } from 'kibana/server'; import { i18n } from '@kbn/i18n'; -import { IndexPatternAttributes } from '../../../common'; +import { IndexPatternAttributes } from '../../common'; type IndexPatternAttributesWithFields = Pick; @@ -41,10 +41,10 @@ export const createScriptedFieldsDeprecationsConfig: ( return [ { - title: i18n.translate('data.deprecations.scriptedFieldsTitle', { + title: i18n.translate('dataViews.deprecations.scriptedFieldsTitle', { defaultMessage: 'Found index patterns using scripted fields', }), - message: i18n.translate('data.deprecations.scriptedFieldsMessage', { + message: i18n.translate('dataViews.deprecations.scriptedFieldsMessage', { defaultMessage: `You have {numberOfIndexPatternsWithScriptedFields} index patterns ({titlesPreview}...) that use scripted fields. Scripted fields are deprecated and will be removed in future. Use runtime fields instead.`, values: { titlesPreview: indexPatternTitles.slice(0, PREVIEW_LIMIT).join('; '), @@ -56,10 +56,10 @@ export const createScriptedFieldsDeprecationsConfig: ( level: 'warning', // warning because it is not set in stone WHEN we remove scripted fields, hence this deprecation is not a blocker for 8.0 upgrade correctiveActions: { manualSteps: [ - i18n.translate('data.deprecations.scriptedFields.manualStepOneMessage', { + i18n.translate('dataViews.deprecations.scriptedFields.manualStepOneMessage', { defaultMessage: 'Navigate to Stack Management > Kibana > Index Patterns.', }), - i18n.translate('data.deprecations.scriptedFields.manualStepTwoMessage', { + i18n.translate('dataViews.deprecations.scriptedFields.manualStepTwoMessage', { defaultMessage: 'Update {numberOfIndexPatternsWithScriptedFields} index patterns that have scripted fields to use runtime fields instead. In most cases, to migrate existing scripts, you will need to change "return ;" to "emit();". Index patterns with at least one scripted field: {allTitles}', values: { diff --git a/src/plugins/data/server/data_views/error.ts b/src/plugins/data_views/server/error.ts similarity index 100% rename from src/plugins/data/server/data_views/error.ts rename to src/plugins/data_views/server/error.ts diff --git a/src/plugins/data/server/data_views/expressions/index.ts b/src/plugins/data_views/server/expressions/index.ts similarity index 100% rename from src/plugins/data/server/data_views/expressions/index.ts rename to src/plugins/data_views/server/expressions/index.ts diff --git a/src/plugins/data/server/data_views/expressions/load_index_pattern.test.ts b/src/plugins/data_views/server/expressions/load_index_pattern.test.ts similarity index 94% rename from src/plugins/data/server/data_views/expressions/load_index_pattern.test.ts rename to src/plugins/data_views/server/expressions/load_index_pattern.test.ts index 370d7dcfd7eb..94bd854e6734 100644 --- a/src/plugins/data/server/data_views/expressions/load_index_pattern.test.ts +++ b/src/plugins/data_views/server/expressions/load_index_pattern.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPatternLoadStartDependencies } from '../../../common/data_views/expressions'; +import { IndexPatternLoadStartDependencies } from '../../common/expressions'; import { getFunctionDefinition } from './load_index_pattern'; describe('indexPattern expression function', () => { diff --git a/src/plugins/data/server/data_views/expressions/load_index_pattern.ts b/src/plugins/data_views/server/expressions/load_index_pattern.ts similarity index 85% rename from src/plugins/data/server/data_views/expressions/load_index_pattern.ts rename to src/plugins/data_views/server/expressions/load_index_pattern.ts index 4585101f2812..8ade41132e14 100644 --- a/src/plugins/data/server/data_views/expressions/load_index_pattern.ts +++ b/src/plugins/data_views/server/expressions/load_index_pattern.ts @@ -13,8 +13,8 @@ import { getIndexPatternLoadMeta, IndexPatternLoadExpressionFunctionDefinition, IndexPatternLoadStartDependencies, -} from '../../../common/data_views/expressions'; -import { DataPluginStartDependencies, DataPluginStart } from '../../plugin'; +} from '../../common/expressions'; +import { DataViewsServerPluginStartDependencies, DataViewsServerPluginStart } from '../types'; /** * Returns the expression function definition. Any stateful dependencies are accessed @@ -39,7 +39,7 @@ export function getFunctionDefinition({ const kibanaRequest = getKibanaRequest ? getKibanaRequest() : null; if (!kibanaRequest) { throw new Error( - i18n.translate('data.indexPatterns.indexPatternLoad.error.kibanaRequest', { + i18n.translate('dataViews.indexPatternLoad.error.kibanaRequest', { defaultMessage: 'A KibanaRequest is required to execute this search on the server. ' + 'Please provide a request object to the expression execution params.', @@ -73,13 +73,17 @@ export function getFunctionDefinition({ export function getIndexPatternLoad({ getStartServices, }: { - getStartServices: StartServicesAccessor; + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + >; }) { return getFunctionDefinition({ getStartDependencies: async (request: KibanaRequest) => { - const [{ elasticsearch, savedObjects }, , { indexPatterns }] = await getStartServices(); + const [{ elasticsearch, savedObjects }, , { indexPatternsServiceFactory }] = + await getStartServices(); return { - indexPatterns: await indexPatterns.indexPatternsServiceFactory( + indexPatterns: await indexPatternsServiceFactory( savedObjects.getScopedClient(request), elasticsearch.client.asScoped(request).asCurrentUser ), diff --git a/src/plugins/data/server/data_views/fetcher/index.ts b/src/plugins/data_views/server/fetcher/index.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/index.ts rename to src/plugins/data_views/server/fetcher/index.ts diff --git a/src/plugins/data_views/server/fetcher/index_not_found_exception.json b/src/plugins/data_views/server/fetcher/index_not_found_exception.json new file mode 100644 index 000000000000..dc892d95ae39 --- /dev/null +++ b/src/plugins/data_views/server/fetcher/index_not_found_exception.json @@ -0,0 +1,21 @@ +{ + "error" : { + "root_cause" : [ + { + "type" : "index_not_found_exception", + "reason" : "no such index [poop]", + "resource.type" : "index_or_alias", + "resource.id" : "poop", + "index_uuid" : "_na_", + "index" : "poop" + } + ], + "type" : "index_not_found_exception", + "reason" : "no such index [poop]", + "resource.type" : "index_or_alias", + "resource.id" : "poop", + "index_uuid" : "_na_", + "index" : "poop" + }, + "status" : 404 +} diff --git a/src/plugins/data/server/data_views/fetcher/index_patterns_fetcher.test.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts similarity index 95% rename from src/plugins/data/server/data_views/fetcher/index_patterns_fetcher.test.ts rename to src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts index 4bd21fb3a182..a65d4d551cf7 100644 --- a/src/plugins/data/server/data_views/fetcher/index_patterns_fetcher.test.ts +++ b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts @@ -8,7 +8,7 @@ import { IndexPatternsFetcher } from '.'; import { ElasticsearchClient } from 'kibana/server'; -import * as indexNotFoundException from '../../../common/search/test_data/index_not_found_exception.json'; +import * as indexNotFoundException from './index_not_found_exception.json'; describe('Index Pattern Fetcher - server', () => { let indexPatterns: IndexPatternsFetcher; diff --git a/src/plugins/data/server/data_views/fetcher/index_patterns_fetcher.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/index_patterns_fetcher.ts rename to src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/errors.ts b/src/plugins/data_views/server/fetcher/lib/errors.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/errors.ts rename to src/plugins/data_views/server/fetcher/lib/errors.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/es_api.test.js b/src/plugins/data_views/server/fetcher/lib/es_api.test.js similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/es_api.test.js rename to src/plugins/data_views/server/fetcher/lib/es_api.test.js diff --git a/src/plugins/data/server/data_views/fetcher/lib/es_api.ts b/src/plugins/data_views/server/fetcher/lib/es_api.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/es_api.ts rename to src/plugins/data_views/server/fetcher/lib/es_api.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/field_capabilities/__fixtures__/es_field_caps_response.json b/src/plugins/data_views/server/fetcher/lib/field_capabilities/__fixtures__/es_field_caps_response.json similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/field_capabilities/__fixtures__/es_field_caps_response.json rename to src/plugins/data_views/server/fetcher/lib/field_capabilities/__fixtures__/es_field_caps_response.json diff --git a/src/plugins/data/server/data_views/fetcher/lib/field_capabilities/field_capabilities.test.js b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.test.js similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/field_capabilities/field_capabilities.test.js rename to src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.test.js diff --git a/src/plugins/data/server/data_views/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/field_capabilities/field_capabilities.ts rename to src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/field_capabilities/field_caps_response.test.js b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_caps_response.test.js similarity index 99% rename from src/plugins/data/server/data_views/fetcher/lib/field_capabilities/field_caps_response.test.js rename to src/plugins/data_views/server/fetcher/lib/field_capabilities/field_caps_response.test.js index c12eff1b5a37..f1e3f314351d 100644 --- a/src/plugins/data/server/data_views/fetcher/lib/field_capabilities/field_caps_response.test.js +++ b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_caps_response.test.js @@ -13,7 +13,7 @@ import sinon from 'sinon'; import * as shouldReadFieldFromDocValuesNS from './should_read_field_from_doc_values'; import { shouldReadFieldFromDocValues } from './should_read_field_from_doc_values'; -import { getKbnFieldType } from '../../../../../common'; +import { getKbnFieldType } from '@kbn/field-types'; import { readFieldCapsResponse } from './field_caps_response'; import esResponse from './__fixtures__/es_field_caps_response.json'; diff --git a/src/plugins/data/server/data_views/fetcher/lib/field_capabilities/field_caps_response.ts b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_caps_response.ts similarity index 98% rename from src/plugins/data/server/data_views/fetcher/lib/field_capabilities/field_caps_response.ts rename to src/plugins/data_views/server/fetcher/lib/field_capabilities/field_caps_response.ts index 3f83fd71b74e..6dff343f9e00 100644 --- a/src/plugins/data/server/data_views/fetcher/lib/field_capabilities/field_caps_response.ts +++ b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_caps_response.ts @@ -8,7 +8,7 @@ import { uniq } from 'lodash'; import type { estypes } from '@elastic/elasticsearch'; -import { castEsToKbnFieldTypeName } from '../../../../../common'; +import { castEsToKbnFieldTypeName } from '@kbn/field-types'; import { shouldReadFieldFromDocValues } from './should_read_field_from_doc_values'; import { FieldDescriptor } from '../../../fetcher'; diff --git a/src/plugins/data/server/data_views/fetcher/lib/field_capabilities/index.ts b/src/plugins/data_views/server/fetcher/lib/field_capabilities/index.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/field_capabilities/index.ts rename to src/plugins/data_views/server/fetcher/lib/field_capabilities/index.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/field_capabilities/overrides.ts b/src/plugins/data_views/server/fetcher/lib/field_capabilities/overrides.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/field_capabilities/overrides.ts rename to src/plugins/data_views/server/fetcher/lib/field_capabilities/overrides.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts b/src/plugins/data_views/server/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts rename to src/plugins/data_views/server/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts b/src/plugins/data_views/server/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts rename to src/plugins/data_views/server/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/index.ts b/src/plugins/data_views/server/fetcher/lib/index.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/index.ts rename to src/plugins/data_views/server/fetcher/lib/index.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/jobs_compatibility.test.js b/src/plugins/data_views/server/fetcher/lib/jobs_compatibility.test.js similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/jobs_compatibility.test.js rename to src/plugins/data_views/server/fetcher/lib/jobs_compatibility.test.js diff --git a/src/plugins/data/server/data_views/fetcher/lib/jobs_compatibility.ts b/src/plugins/data_views/server/fetcher/lib/jobs_compatibility.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/jobs_compatibility.ts rename to src/plugins/data_views/server/fetcher/lib/jobs_compatibility.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/map_capabilities.ts b/src/plugins/data_views/server/fetcher/lib/map_capabilities.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/map_capabilities.ts rename to src/plugins/data_views/server/fetcher/lib/map_capabilities.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/merge_capabilities_with_fields.ts b/src/plugins/data_views/server/fetcher/lib/merge_capabilities_with_fields.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/merge_capabilities_with_fields.ts rename to src/plugins/data_views/server/fetcher/lib/merge_capabilities_with_fields.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/resolve_time_pattern.test.js b/src/plugins/data_views/server/fetcher/lib/resolve_time_pattern.test.js similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/resolve_time_pattern.test.js rename to src/plugins/data_views/server/fetcher/lib/resolve_time_pattern.test.js diff --git a/src/plugins/data/server/data_views/fetcher/lib/resolve_time_pattern.ts b/src/plugins/data_views/server/fetcher/lib/resolve_time_pattern.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/resolve_time_pattern.ts rename to src/plugins/data_views/server/fetcher/lib/resolve_time_pattern.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/time_pattern_to_wildcard.test.ts b/src/plugins/data_views/server/fetcher/lib/time_pattern_to_wildcard.test.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/time_pattern_to_wildcard.test.ts rename to src/plugins/data_views/server/fetcher/lib/time_pattern_to_wildcard.test.ts diff --git a/src/plugins/data/server/data_views/fetcher/lib/time_pattern_to_wildcard.ts b/src/plugins/data_views/server/fetcher/lib/time_pattern_to_wildcard.ts similarity index 100% rename from src/plugins/data/server/data_views/fetcher/lib/time_pattern_to_wildcard.ts rename to src/plugins/data_views/server/fetcher/lib/time_pattern_to_wildcard.ts diff --git a/src/plugins/data/server/data_views/has_user_index_pattern.test.ts b/src/plugins/data_views/server/has_user_index_pattern.test.ts similarity index 99% rename from src/plugins/data/server/data_views/has_user_index_pattern.test.ts rename to src/plugins/data_views/server/has_user_index_pattern.test.ts index efc149b40937..aeaa64b949db 100644 --- a/src/plugins/data/server/data_views/has_user_index_pattern.test.ts +++ b/src/plugins/data_views/server/has_user_index_pattern.test.ts @@ -7,7 +7,7 @@ */ import { hasUserIndexPattern } from './has_user_index_pattern'; -import { elasticsearchServiceMock, savedObjectsClientMock } from '../../../../core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from '../../../core/server/mocks'; describe('hasUserIndexPattern', () => { const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; diff --git a/src/plugins/data/server/data_views/has_user_index_pattern.ts b/src/plugins/data_views/server/has_user_index_pattern.ts similarity index 91% rename from src/plugins/data/server/data_views/has_user_index_pattern.ts rename to src/plugins/data_views/server/has_user_index_pattern.ts index 97abd0892b83..6566f75ebc52 100644 --- a/src/plugins/data/server/data_views/has_user_index_pattern.ts +++ b/src/plugins/data_views/server/has_user_index_pattern.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../core/server'; -import { IndexPatternSavedObjectAttrs } from '../../common/data_views/data_views'; -import { FLEET_ASSETS_TO_IGNORE } from '../../common/data_views/constants'; +import { ElasticsearchClient, SavedObjectsClientContract } from '../../../core/server'; +import { IndexPatternSavedObjectAttrs } from '../common/data_views'; +import { FLEET_ASSETS_TO_IGNORE } from '../common/constants'; interface Deps { esClient: ElasticsearchClient; diff --git a/src/plugins/data_views/server/index.ts b/src/plugins/data_views/server/index.ts new file mode 100644 index 000000000000..1c7eeb073bbe --- /dev/null +++ b/src/plugins/data_views/server/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getFieldByName, findIndexPatternById } from './utils'; +export { + IndexPatternsFetcher, + FieldDescriptor, + shouldReadFieldFromDocValues, + mergeCapabilitiesWithFields, + getCapabilitiesForRollupIndices, +} from './fetcher'; +export { IndexPatternsServiceStart } from './types'; + +import { PluginInitializerContext } from 'src/core/server'; +import { DataViewsServerPlugin } from './plugin'; +import { DataViewsServerPluginSetup, DataViewsServerPluginStart } from './types'; +export type { dataViewsServiceFactory } from './data_views_service_factory'; + +/** + * Static code to be shared externally + * @public + */ + +export function plugin(initializerContext: PluginInitializerContext) { + return new DataViewsServerPlugin(initializerContext); +} + +export { + DataViewsServerPlugin as Plugin, + DataViewsServerPluginSetup as PluginSetup, + DataViewsServerPluginStart as PluginStart, +}; diff --git a/src/plugins/data/server/data_views/index_patterns_api_client.ts b/src/plugins/data_views/server/index_patterns_api_client.ts similarity index 94% rename from src/plugins/data/server/data_views/index_patterns_api_client.ts rename to src/plugins/data_views/server/index_patterns_api_client.ts index 4f71bf218dd4..26ccdd7e02b4 100644 --- a/src/plugins/data/server/data_views/index_patterns_api_client.ts +++ b/src/plugins/data_views/server/index_patterns_api_client.ts @@ -11,8 +11,8 @@ import { GetFieldsOptions, IIndexPatternsApiClient, GetFieldsOptionsTimePattern, -} from '../../common/data_views/types'; -import { DataViewMissingIndices } from '../../common/data_views/lib'; +} from '../common/types'; +import { DataViewMissingIndices } from '../common/lib'; import { IndexPatternsFetcher } from './fetcher'; import { hasUserIndexPattern } from './has_user_index_pattern'; diff --git a/packages/kbn-legacy-logging/src/utils/index.ts b/src/plugins/data_views/server/mocks.ts similarity index 59% rename from packages/kbn-legacy-logging/src/utils/index.ts rename to src/plugins/data_views/server/mocks.ts index 3036671121fe..70a582810a1e 100644 --- a/packages/kbn-legacy-logging/src/utils/index.ts +++ b/src/plugins/data_views/server/mocks.ts @@ -6,5 +6,10 @@ * Side Public License, v 1. */ -export { applyFiltersToKeys } from './apply_filters_to_keys'; -export { getResponsePayloadBytes } from './get_payload_size'; +export function createIndexPatternsStartMock() { + const dataViewsServiceFactory = jest.fn().mockResolvedValue({ get: jest.fn() }); + return { + indexPatternsServiceFactory: dataViewsServiceFactory, + dataViewsServiceFactory, + }; +} diff --git a/src/plugins/data_views/server/plugin.ts b/src/plugins/data_views/server/plugin.ts new file mode 100644 index 000000000000..7285e74847e5 --- /dev/null +++ b/src/plugins/data_views/server/plugin.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; +import { dataViewsServiceFactory } from './data_views_service_factory'; +import { registerRoutes } from './routes'; +import { dataViewSavedObjectType } from './saved_objects'; +import { capabilitiesProvider } from './capabilities_provider'; +import { getIndexPatternLoad } from './expressions'; +import { registerIndexPatternsUsageCollector } from './register_index_pattern_usage_collection'; +import { createScriptedFieldsDeprecationsConfig } from './deprecations'; +import { + DataViewsServerPluginSetup, + DataViewsServerPluginStart, + DataViewsServerPluginSetupDependencies, + DataViewsServerPluginStartDependencies, +} from './types'; + +export class DataViewsServerPlugin + implements + Plugin< + DataViewsServerPluginSetup, + DataViewsServerPluginStart, + DataViewsServerPluginSetupDependencies, + DataViewsServerPluginStartDependencies + > +{ + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get('dataView'); + } + + public setup( + core: CoreSetup, + { expressions, usageCollection }: DataViewsServerPluginSetupDependencies + ) { + core.savedObjects.registerType(dataViewSavedObjectType); + core.capabilities.registerProvider(capabilitiesProvider); + + registerRoutes(core.http, core.getStartServices); + + expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices })); + registerIndexPatternsUsageCollector(core.getStartServices, usageCollection); + core.deprecations.registerDeprecations(createScriptedFieldsDeprecationsConfig(core)); + + return {}; + } + + public start( + { uiSettings }: CoreStart, + { fieldFormats }: DataViewsServerPluginStartDependencies + ) { + const serviceFactory = dataViewsServiceFactory({ + logger: this.logger.get('indexPatterns'), + uiSettings, + fieldFormats, + }); + + return { + indexPatternsServiceFactory: serviceFactory, + dataViewsServiceFactory: serviceFactory, + }; + } + + public stop() {} +} + +export { DataViewsServerPlugin as Plugin }; diff --git a/src/plugins/data/server/data_views/register_index_pattern_usage_collection.test.ts b/src/plugins/data_views/server/register_index_pattern_usage_collection.test.ts similarity index 98% rename from src/plugins/data/server/data_views/register_index_pattern_usage_collection.test.ts rename to src/plugins/data_views/server/register_index_pattern_usage_collection.test.ts index 2c826185757d..01d3a574a58c 100644 --- a/src/plugins/data/server/data_views/register_index_pattern_usage_collection.test.ts +++ b/src/plugins/data_views/server/register_index_pattern_usage_collection.test.ts @@ -12,7 +12,7 @@ import { updateMax, getIndexPatternTelemetry, } from './register_index_pattern_usage_collection'; -import { IndexPatternsCommonService } from '..'; +import { DataViewsService } from '../common'; const scriptA = 'emit(0);'; const scriptB = 'emit(1);\nemit(2);'; @@ -32,7 +32,7 @@ const indexPatterns = { getScriptedFields: () => [], fields: [], }), -} as any as IndexPatternsCommonService; +} as any as DataViewsService; describe('index pattern usage collection', () => { it('minMaxAvgLoC calculates min, max, and average ', () => { diff --git a/src/plugins/data/server/data_views/register_index_pattern_usage_collection.ts b/src/plugins/data_views/server/register_index_pattern_usage_collection.ts similarity index 90% rename from src/plugins/data/server/data_views/register_index_pattern_usage_collection.ts rename to src/plugins/data_views/server/register_index_pattern_usage_collection.ts index 36c2a59ce275..6af2f6df6725 100644 --- a/src/plugins/data/server/data_views/register_index_pattern_usage_collection.ts +++ b/src/plugins/data_views/server/register_index_pattern_usage_collection.ts @@ -8,9 +8,9 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { StartServicesAccessor } from 'src/core/server'; -import { IndexPatternsCommonService } from '..'; -import { SavedObjectsClient } from '../../../../core/server'; -import { DataPluginStartDependencies, DataPluginStart } from '../plugin'; +import { DataViewsService } from '../common'; +import { SavedObjectsClient } from '../../../core/server'; +import { DataViewsServerPluginStartDependencies, DataViewsServerPluginStart } from './types'; interface CountSummary { min?: number; @@ -57,7 +57,7 @@ export const updateMax = (currentMax: number | undefined, newVal: number): numbe } }; -export async function getIndexPatternTelemetry(indexPatterns: IndexPatternsCommonService) { +export async function getIndexPatternTelemetry(indexPatterns: DataViewsService) { const ids = await indexPatterns.getIds(); const countSummaryDefaults: CountSummary = { @@ -139,7 +139,10 @@ export async function getIndexPatternTelemetry(indexPatterns: IndexPatternsCommo } export function registerIndexPatternsUsageCollector( - getStartServices: StartServicesAccessor, + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + >, usageCollection?: UsageCollectionSetup ): void { if (!usageCollection) { @@ -150,8 +153,9 @@ export function registerIndexPatternsUsageCollector( type: 'index-patterns', isReady: () => true, fetch: async () => { - const [{ savedObjects, elasticsearch }, , { indexPatterns }] = await getStartServices(); - const indexPatternService = await indexPatterns.indexPatternsServiceFactory( + const [{ savedObjects, elasticsearch }, , { indexPatternsServiceFactory }] = + await getStartServices(); + const indexPatternService = await indexPatternsServiceFactory( new SavedObjectsClient(savedObjects.createInternalRepository()), elasticsearch.client.asInternalUser ); diff --git a/src/plugins/data/server/data_views/routes.ts b/src/plugins/data_views/server/routes.ts similarity index 96% rename from src/plugins/data/server/data_views/routes.ts rename to src/plugins/data_views/server/routes.ts index 9488285fc7e2..48c359cd9d85 100644 --- a/src/plugins/data/server/data_views/routes.ts +++ b/src/plugins/data_views/server/routes.ts @@ -19,7 +19,7 @@ import { registerPutScriptedFieldRoute } from './routes/scripted_fields/put_scri import { registerGetScriptedFieldRoute } from './routes/scripted_fields/get_scripted_field'; import { registerDeleteScriptedFieldRoute } from './routes/scripted_fields/delete_scripted_field'; import { registerUpdateScriptedFieldRoute } from './routes/scripted_fields/update_scripted_field'; -import type { DataPluginStart, DataPluginStartDependencies } from '../plugin'; +import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies } from './types'; import { registerManageDefaultIndexPatternRoutes } from './routes/default_index_pattern'; import { registerCreateRuntimeFieldRoute } from './routes/runtime_fields/create_runtime_field'; import { registerGetRuntimeFieldRoute } from './routes/runtime_fields/get_runtime_field'; @@ -30,7 +30,10 @@ import { registerHasUserIndexPatternRoute } from './routes/has_user_index_patter export function registerRoutes( http: HttpServiceSetup, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) { const parseMetaFields = (metaFields: string | string[]) => { let parsedFields: string[] = []; diff --git a/src/plugins/data/server/data_views/routes/create_index_pattern.ts b/src/plugins/data_views/server/routes/create_index_pattern.ts similarity index 84% rename from src/plugins/data/server/data_views/routes/create_index_pattern.ts rename to src/plugins/data_views/server/routes/create_index_pattern.ts index 7049903f84e8..b87b03f8bd4a 100644 --- a/src/plugins/data/server/data_views/routes/create_index_pattern.ts +++ b/src/plugins/data_views/server/routes/create_index_pattern.ts @@ -7,15 +7,15 @@ */ import { schema } from '@kbn/config-schema'; -import { IndexPatternSpec } from 'src/plugins/data/common'; +import { IndexPatternSpec } from 'src/plugins/data_views/common'; import { handleErrors } from './util/handle_errors'; import { fieldSpecSchema, runtimeFieldSpecSchema, serializedFieldFormatSchema, } from './util/schemas'; -import { IRouter, StartServicesAccessor } from '../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../core/server'; +import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies } from '../types'; const indexPatternSpecSchema = schema.object({ title: schema.string(), @@ -48,7 +48,10 @@ const indexPatternSpecSchema = schema.object({ export const registerCreateIndexPatternRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.post( { @@ -65,8 +68,8 @@ export const registerCreateIndexPatternRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/default_index_pattern.ts b/src/plugins/data_views/server/routes/default_index_pattern.ts similarity index 76% rename from src/plugins/data/server/data_views/routes/default_index_pattern.ts rename to src/plugins/data_views/server/routes/default_index_pattern.ts index cf5986943eb3..620e201a4850 100644 --- a/src/plugins/data/server/data_views/routes/default_index_pattern.ts +++ b/src/plugins/data_views/server/routes/default_index_pattern.ts @@ -7,13 +7,16 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter, StartServicesAccessor } from '../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../core/server'; +import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies } from '../types'; import { handleErrors } from './util/handle_errors'; export const registerManageDefaultIndexPatternRoutes = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.get( { @@ -23,8 +26,8 @@ export const registerManageDefaultIndexPatternRoutes = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); @@ -57,8 +60,8 @@ export const registerManageDefaultIndexPatternRoutes = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/delete_index_pattern.ts b/src/plugins/data_views/server/routes/delete_index_pattern.ts similarity index 75% rename from src/plugins/data/server/data_views/routes/delete_index_pattern.ts rename to src/plugins/data_views/server/routes/delete_index_pattern.ts index 14de079470dc..0d3f929cdccc 100644 --- a/src/plugins/data/server/data_views/routes/delete_index_pattern.ts +++ b/src/plugins/data_views/server/routes/delete_index_pattern.ts @@ -8,12 +8,15 @@ import { schema } from '@kbn/config-schema'; import { handleErrors } from './util/handle_errors'; -import { IRouter, StartServicesAccessor } from '../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../core/server'; +import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies } from '../types'; export const registerDeleteIndexPatternRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.delete( { @@ -34,8 +37,8 @@ export const registerDeleteIndexPatternRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/fields/update_fields.ts b/src/plugins/data_views/server/routes/fields/update_fields.ts similarity index 87% rename from src/plugins/data/server/data_views/routes/fields/update_fields.ts rename to src/plugins/data_views/server/routes/fields/update_fields.ts index a510fdaa6e1d..3e45ee46f2bb 100644 --- a/src/plugins/data/server/data_views/routes/fields/update_fields.ts +++ b/src/plugins/data_views/server/routes/fields/update_fields.ts @@ -9,12 +9,18 @@ import { schema } from '@kbn/config-schema'; import { handleErrors } from '../util/handle_errors'; import { serializedFieldFormatSchema } from '../util/schemas'; -import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; export const registerUpdateFieldsRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.post( { @@ -55,8 +61,8 @@ export const registerUpdateFieldsRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/get_index_pattern.ts b/src/plugins/data_views/server/routes/get_index_pattern.ts similarity index 76% rename from src/plugins/data/server/data_views/routes/get_index_pattern.ts rename to src/plugins/data_views/server/routes/get_index_pattern.ts index 268fd3da8cd6..7fea748ca338 100644 --- a/src/plugins/data/server/data_views/routes/get_index_pattern.ts +++ b/src/plugins/data_views/server/routes/get_index_pattern.ts @@ -8,12 +8,15 @@ import { schema } from '@kbn/config-schema'; import { handleErrors } from './util/handle_errors'; -import { IRouter, StartServicesAccessor } from '../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../core/server'; +import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies } from '../types'; export const registerGetIndexPatternRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.get( { @@ -34,8 +37,8 @@ export const registerGetIndexPatternRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/has_user_index_pattern.ts b/src/plugins/data_views/server/routes/has_user_index_pattern.ts similarity index 69% rename from src/plugins/data/server/data_views/routes/has_user_index_pattern.ts rename to src/plugins/data_views/server/routes/has_user_index_pattern.ts index 7d67e96f39f6..af0ad1cc88d2 100644 --- a/src/plugins/data/server/data_views/routes/has_user_index_pattern.ts +++ b/src/plugins/data_views/server/routes/has_user_index_pattern.ts @@ -7,12 +7,15 @@ */ import { handleErrors } from './util/handle_errors'; -import { IRouter, StartServicesAccessor } from '../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../core/server'; +import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies } from '../types'; export const registerHasUserIndexPatternRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.get( { @@ -23,8 +26,8 @@ export const registerHasUserIndexPatternRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/runtime_fields/create_runtime_field.ts b/src/plugins/data_views/server/routes/runtime_fields/create_runtime_field.ts similarity index 82% rename from src/plugins/data/server/data_views/routes/runtime_fields/create_runtime_field.ts rename to src/plugins/data_views/server/routes/runtime_fields/create_runtime_field.ts index faf6d87b6d10..04b661d14732 100644 --- a/src/plugins/data/server/data_views/routes/runtime_fields/create_runtime_field.ts +++ b/src/plugins/data_views/server/routes/runtime_fields/create_runtime_field.ts @@ -9,12 +9,18 @@ import { schema } from '@kbn/config-schema'; import { handleErrors } from '../util/handle_errors'; import { runtimeFieldSpecSchema } from '../util/schemas'; -import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; export const registerCreateRuntimeFieldRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.post( { @@ -39,8 +45,8 @@ export const registerCreateRuntimeFieldRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/runtime_fields/delete_runtime_field.ts b/src/plugins/data_views/server/routes/runtime_fields/delete_runtime_field.ts similarity index 79% rename from src/plugins/data/server/data_views/routes/runtime_fields/delete_runtime_field.ts rename to src/plugins/data_views/server/routes/runtime_fields/delete_runtime_field.ts index 58b8529d7cf5..e5c6b03a6422 100644 --- a/src/plugins/data/server/data_views/routes/runtime_fields/delete_runtime_field.ts +++ b/src/plugins/data_views/server/routes/runtime_fields/delete_runtime_field.ts @@ -9,12 +9,18 @@ import { schema } from '@kbn/config-schema'; import { ErrorIndexPatternFieldNotFound } from '../../error'; import { handleErrors } from '../util/handle_errors'; -import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; export const registerDeleteRuntimeFieldRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.delete( { @@ -35,8 +41,8 @@ export const registerDeleteRuntimeFieldRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/runtime_fields/get_runtime_field.ts b/src/plugins/data_views/server/routes/runtime_fields/get_runtime_field.ts similarity index 79% rename from src/plugins/data/server/data_views/routes/runtime_fields/get_runtime_field.ts rename to src/plugins/data_views/server/routes/runtime_fields/get_runtime_field.ts index 6bc2bf396c0b..b457ae6b0159 100644 --- a/src/plugins/data/server/data_views/routes/runtime_fields/get_runtime_field.ts +++ b/src/plugins/data_views/server/routes/runtime_fields/get_runtime_field.ts @@ -9,12 +9,18 @@ import { schema } from '@kbn/config-schema'; import { ErrorIndexPatternFieldNotFound } from '../../error'; import { handleErrors } from '../util/handle_errors'; -import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; export const registerGetRuntimeFieldRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.get( { @@ -36,8 +42,8 @@ export const registerGetRuntimeFieldRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/runtime_fields/put_runtime_field.ts b/src/plugins/data_views/server/routes/runtime_fields/put_runtime_field.ts similarity index 82% rename from src/plugins/data/server/data_views/routes/runtime_fields/put_runtime_field.ts rename to src/plugins/data_views/server/routes/runtime_fields/put_runtime_field.ts index a5e92fa5a36e..1c3ed99fdf67 100644 --- a/src/plugins/data/server/data_views/routes/runtime_fields/put_runtime_field.ts +++ b/src/plugins/data_views/server/routes/runtime_fields/put_runtime_field.ts @@ -9,12 +9,18 @@ import { schema } from '@kbn/config-schema'; import { handleErrors } from '../util/handle_errors'; import { runtimeFieldSpecSchema } from '../util/schemas'; -import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; export const registerPutRuntimeFieldRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.put( { @@ -38,8 +44,8 @@ export const registerPutRuntimeFieldRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/runtime_fields/update_runtime_field.ts b/src/plugins/data_views/server/routes/runtime_fields/update_runtime_field.ts similarity index 83% rename from src/plugins/data/server/data_views/routes/runtime_fields/update_runtime_field.ts rename to src/plugins/data_views/server/routes/runtime_fields/update_runtime_field.ts index 3f3aae46c438..ca92f310ff28 100644 --- a/src/plugins/data/server/data_views/routes/runtime_fields/update_runtime_field.ts +++ b/src/plugins/data_views/server/routes/runtime_fields/update_runtime_field.ts @@ -7,16 +7,22 @@ */ import { schema } from '@kbn/config-schema'; -import { RuntimeField } from 'src/plugins/data/common'; +import { RuntimeField } from 'src/plugins/data_views/common'; import { ErrorIndexPatternFieldNotFound } from '../../error'; import { handleErrors } from '../util/handle_errors'; import { runtimeFieldSpec, runtimeFieldSpecTypeSchema } from '../util/schemas'; -import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; export const registerUpdateRuntimeFieldRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.post( { @@ -46,8 +52,8 @@ export const registerUpdateRuntimeFieldRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/scripted_fields/create_scripted_field.ts b/src/plugins/data_views/server/routes/scripted_fields/create_scripted_field.ts similarity index 83% rename from src/plugins/data/server/data_views/routes/scripted_fields/create_scripted_field.ts rename to src/plugins/data_views/server/routes/scripted_fields/create_scripted_field.ts index 4d7b1d87cd9e..e620960afbe1 100644 --- a/src/plugins/data/server/data_views/routes/scripted_fields/create_scripted_field.ts +++ b/src/plugins/data_views/server/routes/scripted_fields/create_scripted_field.ts @@ -9,12 +9,18 @@ import { schema } from '@kbn/config-schema'; import { handleErrors } from '../util/handle_errors'; import { fieldSpecSchema } from '../util/schemas'; -import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; export const registerCreateScriptedFieldRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.post( { @@ -38,8 +44,8 @@ export const registerCreateScriptedFieldRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/scripted_fields/delete_scripted_field.ts b/src/plugins/data_views/server/routes/scripted_fields/delete_scripted_field.ts similarity index 81% rename from src/plugins/data/server/data_views/routes/scripted_fields/delete_scripted_field.ts rename to src/plugins/data_views/server/routes/scripted_fields/delete_scripted_field.ts index 169351c220ec..bd1bfe0ec4e2 100644 --- a/src/plugins/data/server/data_views/routes/scripted_fields/delete_scripted_field.ts +++ b/src/plugins/data_views/server/routes/scripted_fields/delete_scripted_field.ts @@ -9,12 +9,18 @@ import { schema } from '@kbn/config-schema'; import { ErrorIndexPatternFieldNotFound } from '../../error'; import { handleErrors } from '../util/handle_errors'; -import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; export const registerDeleteScriptedFieldRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.delete( { @@ -39,8 +45,8 @@ export const registerDeleteScriptedFieldRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/scripted_fields/get_scripted_field.ts b/src/plugins/data_views/server/routes/scripted_fields/get_scripted_field.ts similarity index 81% rename from src/plugins/data/server/data_views/routes/scripted_fields/get_scripted_field.ts rename to src/plugins/data_views/server/routes/scripted_fields/get_scripted_field.ts index 28f3f75a7aa1..ae9cca2c79b4 100644 --- a/src/plugins/data/server/data_views/routes/scripted_fields/get_scripted_field.ts +++ b/src/plugins/data_views/server/routes/scripted_fields/get_scripted_field.ts @@ -9,12 +9,18 @@ import { schema } from '@kbn/config-schema'; import { ErrorIndexPatternFieldNotFound } from '../../error'; import { handleErrors } from '../util/handle_errors'; -import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; export const registerGetScriptedFieldRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.get( { @@ -39,8 +45,8 @@ export const registerGetScriptedFieldRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/scripted_fields/put_scripted_field.ts b/src/plugins/data_views/server/routes/scripted_fields/put_scripted_field.ts similarity index 83% rename from src/plugins/data/server/data_views/routes/scripted_fields/put_scripted_field.ts rename to src/plugins/data_views/server/routes/scripted_fields/put_scripted_field.ts index 368ad53eb225..a6cee3762513 100644 --- a/src/plugins/data/server/data_views/routes/scripted_fields/put_scripted_field.ts +++ b/src/plugins/data_views/server/routes/scripted_fields/put_scripted_field.ts @@ -9,12 +9,18 @@ import { schema } from '@kbn/config-schema'; import { handleErrors } from '../util/handle_errors'; import { fieldSpecSchema } from '../util/schemas'; -import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; export const registerPutScriptedFieldRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.put( { @@ -38,8 +44,8 @@ export const registerPutScriptedFieldRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/scripted_fields/update_scripted_field.ts b/src/plugins/data_views/server/routes/scripted_fields/update_scripted_field.ts similarity index 86% rename from src/plugins/data/server/data_views/routes/scripted_fields/update_scripted_field.ts rename to src/plugins/data_views/server/routes/scripted_fields/update_scripted_field.ts index bf10a3ee6389..2917838293ec 100644 --- a/src/plugins/data/server/data_views/routes/scripted_fields/update_scripted_field.ts +++ b/src/plugins/data_views/server/routes/scripted_fields/update_scripted_field.ts @@ -7,16 +7,22 @@ */ import { schema } from '@kbn/config-schema'; -import { FieldSpec } from 'src/plugins/data/common'; +import { FieldSpec } from 'src/plugins/data_views/common'; import { ErrorIndexPatternFieldNotFound } from '../../error'; import { handleErrors } from '../util/handle_errors'; import { fieldSpecSchemaFields } from '../util/schemas'; -import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; export const registerUpdateScriptedFieldRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.post( { @@ -59,8 +65,8 @@ export const registerUpdateScriptedFieldRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/update_index_pattern.ts b/src/plugins/data_views/server/routes/update_index_pattern.ts similarity index 91% rename from src/plugins/data/server/data_views/routes/update_index_pattern.ts rename to src/plugins/data_views/server/routes/update_index_pattern.ts index 1c88550c154c..1421057d65d2 100644 --- a/src/plugins/data/server/data_views/routes/update_index_pattern.ts +++ b/src/plugins/data_views/server/routes/update_index_pattern.ts @@ -13,8 +13,8 @@ import { runtimeFieldSpecSchema, serializedFieldFormatSchema, } from './util/schemas'; -import { IRouter, StartServicesAccessor } from '../../../../../core/server'; -import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; +import { IRouter, StartServicesAccessor } from '../../../../core/server'; +import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies } from '../types'; const indexPatternUpdateSchema = schema.object({ title: schema.maybe(schema.string()), @@ -37,7 +37,10 @@ const indexPatternUpdateSchema = schema.object({ export const registerUpdateIndexPatternRoute = ( router: IRouter, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + > ) => { router.post( { @@ -62,8 +65,8 @@ export const registerUpdateIndexPatternRoute = ( handleErrors(async (ctx, req, res) => { const savedObjectsClient = ctx.core.savedObjects.client; const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; - const [, , { indexPatterns }] = await getStartServices(); - const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + const [, , { indexPatternsServiceFactory }] = await getStartServices(); + const indexPatternsService = await indexPatternsServiceFactory( savedObjectsClient, elasticsearchClient ); diff --git a/src/plugins/data/server/data_views/routes/util/handle_errors.ts b/src/plugins/data_views/server/routes/util/handle_errors.ts similarity index 100% rename from src/plugins/data/server/data_views/routes/util/handle_errors.ts rename to src/plugins/data_views/server/routes/util/handle_errors.ts diff --git a/src/plugins/data/server/data_views/routes/util/schemas.ts b/src/plugins/data_views/server/routes/util/schemas.ts similarity index 96% rename from src/plugins/data/server/data_views/routes/util/schemas.ts rename to src/plugins/data_views/server/routes/util/schemas.ts index 79ee1ffa1ab9..79f493f30380 100644 --- a/src/plugins/data/server/data_views/routes/util/schemas.ts +++ b/src/plugins/data_views/server/routes/util/schemas.ts @@ -7,7 +7,7 @@ */ import { schema, Type } from '@kbn/config-schema'; -import { RUNTIME_FIELD_TYPES, RuntimeType } from '../../../../common'; +import { RUNTIME_FIELD_TYPES, RuntimeType } from '../../../common'; export const serializedFieldFormatSchema = schema.object({ id: schema.maybe(schema.string()), diff --git a/src/plugins/data/server/saved_objects/index_patterns.ts b/src/plugins/data_views/server/saved_objects/data_views.ts similarity index 88% rename from src/plugins/data/server/saved_objects/index_patterns.ts rename to src/plugins/data_views/server/saved_objects/data_views.ts index a809f2ce73e1..d34073287323 100644 --- a/src/plugins/data/server/saved_objects/index_patterns.ts +++ b/src/plugins/data_views/server/saved_objects/data_views.ts @@ -8,10 +8,10 @@ import type { SavedObjectsType } from 'kibana/server'; import { indexPatternSavedObjectTypeMigrations } from './index_pattern_migrations'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common'; -export const indexPatternSavedObjectType: SavedObjectsType = { - name: INDEX_PATTERN_SAVED_OBJECT_TYPE, +export const dataViewSavedObjectType: SavedObjectsType = { + name: DATA_VIEW_SAVED_OBJECT_TYPE, hidden: false, namespaceType: 'single', management: { diff --git a/packages/kbn-config/src/legacy/index.ts b/src/plugins/data_views/server/saved_objects/index.ts similarity index 76% rename from packages/kbn-config/src/legacy/index.ts rename to src/plugins/data_views/server/saved_objects/index.ts index f6906f81d182..ff0f524ae961 100644 --- a/packages/kbn-config/src/legacy/index.ts +++ b/src/plugins/data_views/server/saved_objects/index.ts @@ -6,7 +6,4 @@ * Side Public License, v 1. */ -export { - LegacyObjectToConfigAdapter, - LegacyLoggingConfig, -} from './legacy_object_to_config_adapter'; +export { dataViewSavedObjectType } from './data_views'; diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.test.ts b/src/plugins/data_views/server/saved_objects/index_pattern_migrations.test.ts similarity index 100% rename from src/plugins/data/server/saved_objects/index_pattern_migrations.test.ts rename to src/plugins/data_views/server/saved_objects/index_pattern_migrations.test.ts diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts b/src/plugins/data_views/server/saved_objects/index_pattern_migrations.ts similarity index 100% rename from src/plugins/data/server/saved_objects/index_pattern_migrations.ts rename to src/plugins/data_views/server/saved_objects/index_pattern_migrations.ts diff --git a/src/plugins/data/common/utils/shorten_dotted_string.ts b/src/plugins/data_views/server/saved_objects/migrations/to_v7_12_0.ts similarity index 57% rename from src/plugins/data/common/utils/shorten_dotted_string.ts rename to src/plugins/data_views/server/saved_objects/migrations/to_v7_12_0.ts index 53f7471913dc..955028c0f9bf 100644 --- a/src/plugins/data/common/utils/shorten_dotted_string.ts +++ b/src/plugins/data_views/server/saved_objects/migrations/to_v7_12_0.ts @@ -6,14 +6,12 @@ * Side Public License, v 1. */ -const DOT_PREFIX_RE = /(.).+?\./g; +import type { SavedObjectMigrationFn } from 'kibana/server'; /** - * Convert a dot.notated.string into a short - * version (d.n.string) - * - * @return {any} + * Drop the previous document's attributes, which report `averageDuration` incorrectly. + * @param doc */ -export function shortenDottedString(input: any) { - return typeof input !== 'string' ? input : input.replace(DOT_PREFIX_RE, '$1.'); -} +export const migrate712: SavedObjectMigrationFn = (doc) => { + return { ...doc, attributes: {} }; +}; diff --git a/src/plugins/data/server/data_views/saved_objects_client_wrapper.test.ts b/src/plugins/data_views/server/saved_objects_client_wrapper.test.ts similarity index 96% rename from src/plugins/data/server/data_views/saved_objects_client_wrapper.test.ts rename to src/plugins/data_views/server/saved_objects_client_wrapper.test.ts index bbe857894b3f..b03532421eca 100644 --- a/src/plugins/data/server/data_views/saved_objects_client_wrapper.test.ts +++ b/src/plugins/data_views/server/saved_objects_client_wrapper.test.ts @@ -9,7 +9,7 @@ import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper'; import { SavedObjectsClientContract } from 'src/core/server'; -import { DataViewSavedObjectConflictError } from '../../common/data_views'; +import { DataViewSavedObjectConflictError } from '../common'; describe('SavedObjectsClientPublicToCommon', () => { const soClient = { resolve: jest.fn() } as unknown as SavedObjectsClientContract; diff --git a/src/plugins/data/server/data_views/saved_objects_client_wrapper.ts b/src/plugins/data_views/server/saved_objects_client_wrapper.ts similarity index 98% rename from src/plugins/data/server/data_views/saved_objects_client_wrapper.ts rename to src/plugins/data_views/server/saved_objects_client_wrapper.ts index b37648a3f038..dc7163c405d4 100644 --- a/src/plugins/data/server/data_views/saved_objects_client_wrapper.ts +++ b/src/plugins/data_views/server/saved_objects_client_wrapper.ts @@ -11,7 +11,7 @@ import { SavedObjectsClientCommon, SavedObjectsClientCommonFindArgs, DataViewSavedObjectConflictError, -} from '../../common/data_views'; +} from '../common'; export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommon { private savedObjectClient: SavedObjectsClientContract; diff --git a/src/plugins/data_views/server/types.ts b/src/plugins/data_views/server/types.ts new file mode 100644 index 000000000000..4a57a1d01b9c --- /dev/null +++ b/src/plugins/data_views/server/types.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Logger, SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; +import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { DataViewsService } from '../common'; +import { FieldFormatsSetup, FieldFormatsStart } from '../../field_formats/server'; + +type ServiceFactory = ( + savedObjectsClient: SavedObjectsClientContract, + elasticsearchClient: ElasticsearchClient +) => Promise; +export interface DataViewsServerPluginStart { + dataViewsServiceFactory: ServiceFactory; + /** + * @deprecated Renamed to dataViewsServiceFactory + */ + indexPatternsServiceFactory: ServiceFactory; +} + +export interface IndexPatternsServiceSetupDeps { + expressions: ExpressionsServerSetup; + usageCollection?: UsageCollectionSetup; +} + +export interface IndexPatternsServiceStartDeps { + fieldFormats: FieldFormatsStart; + logger: Logger; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DataViewsServerPluginSetup {} + +export type IndexPatternsServiceStart = DataViewsServerPluginStart; + +export interface DataViewsServerPluginSetupDependencies { + fieldFormats: FieldFormatsSetup; + expressions: ExpressionsServerSetup; + usageCollection?: UsageCollectionSetup; +} + +export interface DataViewsServerPluginStartDependencies { + fieldFormats: FieldFormatsStart; + logger: Logger; +} diff --git a/src/plugins/data/server/data_views/ui_settings_wrapper.ts b/src/plugins/data_views/server/ui_settings_wrapper.ts similarity index 95% rename from src/plugins/data/server/data_views/ui_settings_wrapper.ts rename to src/plugins/data_views/server/ui_settings_wrapper.ts index dce552205db2..f42d43c1c24f 100644 --- a/src/plugins/data/server/data_views/ui_settings_wrapper.ts +++ b/src/plugins/data_views/server/ui_settings_wrapper.ts @@ -7,7 +7,7 @@ */ import { IUiSettingsClient } from 'src/core/server'; -import { UiSettingsCommon } from '../../common'; +import { UiSettingsCommon } from '../common'; export class UiSettingsServerToCommon implements UiSettingsCommon { private uiSettings: IUiSettingsClient; diff --git a/src/plugins/data/server/data_views/utils.ts b/src/plugins/data_views/server/utils.ts similarity index 92% rename from src/plugins/data/server/data_views/utils.ts rename to src/plugins/data_views/server/utils.ts index 7f1a953c482d..bb7d23f83223 100644 --- a/src/plugins/data/server/data_views/utils.ts +++ b/src/plugins/data_views/server/utils.ts @@ -9,10 +9,10 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { IFieldType, - INDEX_PATTERN_SAVED_OBJECT_TYPE, + DATA_VIEW_SAVED_OBJECT_TYPE, IndexPatternAttributes, SavedObject, -} from '../../common'; +} from '../common'; export const getFieldByName = ( fieldName: string, @@ -29,7 +29,7 @@ export const findIndexPatternById = async ( index: string ): Promise | undefined> => { const savedObjectsResponse = await savedObjectsClient.find({ - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, fields: ['fields'], search: `"${index}"`, searchFields: ['title'], diff --git a/src/plugins/data_views/tsconfig.json b/src/plugins/data_views/tsconfig.json new file mode 100644 index 000000000000..f5c80ce30cce --- /dev/null +++ b/src/plugins/data_views/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "config.ts", + "common/**/*.json", + "public/**/*.json", + "server/**/*.json" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../field_formats/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" } + ] +} diff --git a/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx b/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx index c9e0c43900ba..6c1b1bfc87d2 100644 --- a/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx +++ b/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx @@ -32,7 +32,7 @@ export const DiscoverUninitialized = ({ onRefresh }: Props) => {

} actions={ - + { @@ -46,7 +46,6 @@ export function DiscoverMainApp(props: DiscoverMainProps) { }, [history] ); - const savedSearch = props.savedSearch; /** * State related logic diff --git a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx index 5141908e44ad..a95668642558 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx @@ -75,8 +75,6 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { async function loadSavedSearch() { try { - // force a refresh if a given saved search without id was saved - setSavedSearch(undefined); const loadedSavedSearch = await services.getSavedSearchById(savedSearchId); const loadedIndexPattern = await loadDefaultOrCurrentIndexPattern(loadedSavedSearch); if (loadedSavedSearch && !loadedSavedSearch?.searchSource.getField('index')) { diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index e11a9937111a..223d896b16cd 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -96,6 +96,7 @@ export function useDiscoverState({ useEffect(() => { const stopSync = stateContainer.initializeAndSync(indexPattern, filterManager, data); + return () => stopSync(); }, [stateContainer, filterManager, data, indexPattern]); @@ -209,16 +210,13 @@ export function useDiscoverState({ }, [config, data, savedSearch, reset, stateContainer]); /** - * Initial data fetching, also triggered when index pattern changes + * Trigger data fetching on indexPattern or savedSearch changes */ useEffect(() => { - if (!indexPattern) { - return; - } - if (initialFetchStatus === FetchStatus.LOADING) { + if (indexPattern) { refetch$.next(); } - }, [initialFetchStatus, refetch$, indexPattern]); + }, [initialFetchStatus, refetch$, indexPattern, savedSearch.id]); return { data$, diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts index 164dff862779..d11c76283fed 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts @@ -6,15 +6,14 @@ * Side Public License, v 1. */ import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { BehaviorSubject, merge, Subject } from 'rxjs'; -import { debounceTime, filter, tap } from 'rxjs/operators'; +import { BehaviorSubject, Subject } from 'rxjs'; import { DiscoverServices } from '../../../../build_services'; import { DiscoverSearchSessionManager } from './discover_search_session'; import { SearchSource } from '../../../../../../data/common'; import { GetStateReturn } from './discover_state'; import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; import { RequestAdapter } from '../../../../../../inspector/public'; -import { AutoRefreshDoneFn } from '../../../../../../data/public'; +import type { AutoRefreshDoneFn } from '../../../../../../data/public'; import { validateTimeRange } from '../utils/validate_time_range'; import { Chart } from '../components/chart/point_series'; import { useSingleton } from '../utils/use_singleton'; @@ -23,6 +22,7 @@ import { FetchStatus } from '../../../types'; import { fetchAll } from '../utils/fetch_all'; import { useBehaviorSubject } from '../utils/use_behavior_subject'; import { sendResetMsg } from './use_saved_search_messages'; +import { getFetch$ } from '../utils/get_fetch_observable'; export interface SavedSearchData { main$: DataMain$; @@ -134,6 +134,7 @@ export const useSavedSearch = ({ */ const refs = useRef<{ abortController?: AbortController; + autoRefreshDone?: AutoRefreshDoneFn; }>({}); /** @@ -145,29 +146,18 @@ export const useSavedSearch = ({ * handler emitted by `timefilter.getAutoRefreshFetch$()` * to notify when data completed loading and to start a new autorefresh loop */ - let autoRefreshDoneCb: AutoRefreshDoneFn | undefined; - const fetch$ = merge( + const setAutoRefreshDone = (fn: AutoRefreshDoneFn | undefined) => { + refs.current.autoRefreshDone = fn; + }; + const fetch$ = getFetch$({ + setAutoRefreshDone, + data, + main$, refetch$, - filterManager.getFetches$(), - timefilter.getFetch$(), - timefilter.getAutoRefreshFetch$().pipe( - tap((done) => { - autoRefreshDoneCb = done; - }), - filter(() => { - /** - * filter to prevent auto-refresh triggered fetch when - * loading is still ongoing - */ - const currentFetchStatus = main$.getValue().fetchStatus; - return ( - currentFetchStatus !== FetchStatus.LOADING && currentFetchStatus !== FetchStatus.PARTIAL - ); - }) - ), - data.query.queryString.getUpdates$(), - searchSessionManager.newSearchSessionIdFromURL$.pipe(filter((sessionId) => !!sessionId)) - ).pipe(debounceTime(100)); + searchSessionManager, + searchSource, + initialFetchStatus, + }); const subscription = fetch$.subscribe((val) => { if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) { @@ -190,8 +180,8 @@ export const useSavedSearch = ({ }).subscribe({ complete: () => { // if this function was set and is executed, another refresh fetch can be triggered - autoRefreshDoneCb?.(); - autoRefreshDoneCb = undefined; + refs.current.autoRefreshDone?.(); + refs.current.autoRefreshDone = undefined; }, }); } catch (error) { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts new file mode 100644 index 000000000000..528f0e74d3ed --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { merge } from 'rxjs'; +import { debounceTime, filter, skip, tap } from 'rxjs/operators'; + +import { FetchStatus } from '../../../types'; +import type { + AutoRefreshDoneFn, + DataPublicPluginStart, + SearchSource, +} from '../../../../../../data/public'; +import { DataMain$, DataRefetch$ } from '../services/use_saved_search'; +import { DiscoverSearchSessionManager } from '../services/discover_search_session'; + +/** + * This function returns an observable that's used to trigger data fetching + */ +export function getFetch$({ + setAutoRefreshDone, + data, + main$, + refetch$, + searchSessionManager, + initialFetchStatus, +}: { + setAutoRefreshDone: (val: AutoRefreshDoneFn | undefined) => void; + data: DataPublicPluginStart; + main$: DataMain$; + refetch$: DataRefetch$; + searchSessionManager: DiscoverSearchSessionManager; + searchSource: SearchSource; + initialFetchStatus: FetchStatus; +}) { + const { timefilter } = data.query.timefilter; + const { filterManager } = data.query; + let fetch$ = merge( + refetch$, + filterManager.getFetches$(), + timefilter.getFetch$(), + timefilter.getAutoRefreshFetch$().pipe( + tap((done) => { + setAutoRefreshDone(done); + }), + filter(() => { + const currentFetchStatus = main$.getValue().fetchStatus; + return ( + /** + * filter to prevent auto-refresh triggered fetch when + * loading is still ongoing + */ + currentFetchStatus !== FetchStatus.LOADING && currentFetchStatus !== FetchStatus.PARTIAL + ); + }) + ), + data.query.queryString.getUpdates$(), + searchSessionManager.newSearchSessionIdFromURL$.pipe(filter((sessionId) => !!sessionId)) + ).pipe(debounceTime(100)); + + /** + * Skip initial fetch when discover:searchOnPageLoad is disabled. + */ + if (initialFetchStatus === FetchStatus.UNINITIALIZED) { + fetch$ = fetch$.pipe(skip(1)); + } + + return fetch$; +} diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts new file mode 100644 index 000000000000..39873ff609d6 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { getFetch$ } from './get_fetch_observable'; +import { FetchStatus } from '../../../types'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { DataPublicPluginStart } from '../../../../../../data/public'; +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { DataRefetch$ } from '../services/use_saved_search'; +import { savedSearchMock, savedSearchMockWithTimeField } from '../../../../__mocks__/saved_search'; + +function createDataMock( + queryString$: Subject, + filterManager$: Subject, + timefilterFetch$: Subject, + autoRefreshFetch$: Subject +) { + return { + query: { + queryString: { + getUpdates$: () => { + return queryString$; + }, + }, + filterManager: { + getFetches$: () => { + return filterManager$; + }, + }, + timefilter: { + timefilter: { + getFetch$: () => { + return timefilterFetch$; + }, + getAutoRefreshFetch$: () => { + return autoRefreshFetch$; + }, + }, + }, + }, + } as unknown as DataPublicPluginStart; +} + +describe('getFetchObservable', () => { + test('refetch$.next should trigger fetch$.next', async (done) => { + const searchSessionManagerMock = createSearchSessionMock(); + + const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }); + const refetch$: DataRefetch$ = new Subject(); + const fetch$ = getFetch$({ + setAutoRefreshDone: jest.fn(), + main$, + refetch$, + data: createDataMock(new Subject(), new Subject(), new Subject(), new Subject()), + searchSessionManager: searchSessionManagerMock.searchSessionManager, + searchSource: savedSearchMock.searchSource, + initialFetchStatus: FetchStatus.LOADING, + }); + + fetch$.subscribe(() => { + done(); + }); + refetch$.next(); + }); + test('getAutoRefreshFetch$ should trigger fetch$.next', async () => { + jest.useFakeTimers(); + const searchSessionManagerMock = createSearchSessionMock(); + const autoRefreshFetch$ = new Subject(); + + const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }); + const refetch$: DataRefetch$ = new Subject(); + const dataMock = createDataMock(new Subject(), new Subject(), new Subject(), autoRefreshFetch$); + const setAutoRefreshDone = jest.fn(); + const fetch$ = getFetch$({ + setAutoRefreshDone, + main$, + refetch$, + data: dataMock, + searchSessionManager: searchSessionManagerMock.searchSessionManager, + searchSource: savedSearchMockWithTimeField.searchSource, + initialFetchStatus: FetchStatus.LOADING, + }); + + const fetchfnMock = jest.fn(); + fetch$.subscribe(() => { + fetchfnMock(); + }); + autoRefreshFetch$.next(jest.fn()); + jest.runAllTimers(); + expect(fetchfnMock).toHaveBeenCalledTimes(1); + expect(setAutoRefreshDone).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts index 554aca6ddb8f..04ee5f414e7f 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts @@ -31,6 +31,7 @@ describe('getStateDefaults', () => { "index": "index-pattern-with-timefield-id", "interval": "auto", "query": undefined, + "savedQuery": undefined, "sort": Array [ Array [ "timestamp", @@ -59,6 +60,7 @@ describe('getStateDefaults', () => { "index": "the-index-pattern-id", "interval": "auto", "query": undefined, + "savedQuery": undefined, "sort": Array [], } `); diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts index 4061d9a61f0a..cd23d5202237 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts @@ -47,6 +47,7 @@ export function getStateDefaults({ interval: 'auto', filters: cloneDeep(searchSource.getOwnField('filter')), hideChart: undefined, + savedQuery: undefined, } as AppState; if (savedSearch.grid) { defaultState.grid = savedSearch.grid; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx index be49a7697afc..7d8d00156e54 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx @@ -13,7 +13,7 @@ import { useRequest } from '../../../public/request'; import { Privileges, Error as CustomError } from '../types'; -interface Authorization { +export interface Authorization { isLoading: boolean; apiError: CustomError | null; privileges: Privileges; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts index f8eb7e3c7c0c..75d79a204f14 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts @@ -10,6 +10,7 @@ export { AuthorizationProvider, AuthorizationContext, useAuthorizationContext, + Authorization, } from './authorization_provider'; export { WithPrivileges } from './with_privileges'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.test.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.test.ts new file mode 100644 index 000000000000..243bfdb995f5 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { convertPrivilegesToArray } from './with_privileges'; + +describe('convertPrivilegesToArray', () => { + test('extracts section and privilege', () => { + expect(convertPrivilegesToArray('index.index_name')).toEqual([['index', 'index_name']]); + expect(convertPrivilegesToArray(['index.index_name', 'cluster.management'])).toEqual([ + ['index', 'index_name'], + ['cluster', 'management'], + ]); + expect(convertPrivilegesToArray('index.index_name.with-many.dots')).toEqual([ + ['index', 'index_name.with-many.dots'], + ]); + }); + + test('throws when it cannot extract section and privilege', () => { + expect(() => { + convertPrivilegesToArray('bad_privilege_string'); + }).toThrow('Required privilege must have the format "section.privilege"'); + }); +}); diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx index c0e675877c56..6485bd7f45e5 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx @@ -10,13 +10,14 @@ import { MissingPrivileges } from '../types'; import { useAuthorizationContext } from './authorization_provider'; +type Privileges = string | string[]; interface Props { /** * Each required privilege must have the format "section.privilege". * To indicate that *all* privileges from a section are required, we can use the asterix * e.g. "index.*" */ - privileges: string | string[]; + privileges: Privileges; children: (childrenProps: { isLoading: boolean; hasPrivileges: boolean; @@ -26,24 +27,30 @@ interface Props { type Privilege = [string, string]; -const toArray = (value: string | string[]): string[] => +const toArray = (value: Privileges): string[] => Array.isArray(value) ? (value as string[]) : ([value] as string[]); -export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => { - const { isLoading, privileges } = useAuthorizationContext(); - - const privilegesToArray: Privilege[] = toArray(requiredPrivileges).map((p) => { - const [section, privilege] = p.split('.'); - if (!privilege) { - // Oh! we forgot to use the dot "." notation. +export const convertPrivilegesToArray = (privileges: Privileges): Privilege[] => { + return toArray(privileges).map((p) => { + // Since an privilege can contain a dot in its name: + // * `section` needs to be extracted from the beginning of the string until the first dot + // * `privilege` should be everything after the dot + const indexOfFirstPeriod = p.indexOf('.'); + if (indexOfFirstPeriod === -1) { throw new Error('Required privilege must have the format "section.privilege"'); } - return [section, privilege]; + + return [p.slice(0, indexOfFirstPeriod), p.slice(indexOfFirstPeriod + 1)]; }); +}; + +export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => { + const { isLoading, privileges } = useAuthorizationContext(); + const privilegesArray = convertPrivilegesToArray(requiredPrivileges); const hasPrivileges = isLoading ? false - : privilegesToArray.every((privilege) => { + : privilegesArray.every((privilege) => { const [section, requiredPrivilege] = privilege; if (!privileges.missingPrivileges[section]) { // if the section does not exist in our missingPriviledges, everything is OK @@ -61,7 +68,7 @@ export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Pro return !privileges.missingPrivileges[section]!.includes(requiredPrivilege); }); - const privilegesMissing = privilegesToArray.reduce((acc, [section, privilege]) => { + const privilegesMissing = privilegesArray.reduce((acc, [section, privilege]) => { if (privilege === '*') { acc[section] = privileges.missingPrivileges[section] || []; } else if ( diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts index e63d98512a2c..9ccbc5a5cd3d 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts @@ -14,6 +14,7 @@ export { SectionError, PageError, useAuthorizationContext, + Authorization, } from './components'; export { Privileges, MissingPrivileges, Error } from './types'; diff --git a/src/plugins/es_ui_shared/common/index.ts b/src/plugins/es_ui_shared/common/index.ts index b8cfe0ae4858..1c2955b8e5e2 100644 --- a/src/plugins/es_ui_shared/common/index.ts +++ b/src/plugins/es_ui_shared/common/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { Privileges, MissingPrivileges } from '../__packages_do_not_import__/authorization'; +export { Privileges, MissingPrivileges } from '../__packages_do_not_import__/authorization/types'; diff --git a/src/plugins/es_ui_shared/public/authorization/index.ts b/src/plugins/es_ui_shared/public/authorization/index.ts index f68ad3da2a4b..b8fb2f45794e 100644 --- a/src/plugins/es_ui_shared/public/authorization/index.ts +++ b/src/plugins/es_ui_shared/public/authorization/index.ts @@ -17,4 +17,5 @@ export { PageError, useAuthorizationContext, WithPrivileges, + Authorization, } from '../../__packages_do_not_import__/authorization'; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 9db00bc4be8d..2dc50536ca63 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -45,6 +45,7 @@ export { PageError, Error, useAuthorizationContext, + Authorization, } from './authorization'; export { Forms, ace, GlobalFlyout, XJson }; diff --git a/src/plugins/expressions/.eslintrc.json b/src/plugins/expressions/.eslintrc.json index 2aab6c2d9093..d1dbca41acc8 100644 --- a/src/plugins/expressions/.eslintrc.json +++ b/src/plugins/expressions/.eslintrc.json @@ -1,5 +1,6 @@ { "rules": { - "@typescript-eslint/consistent-type-definitions": 0 + "@typescript-eslint/consistent-type-definitions": 0, + "@typescript-eslint/no-explicit-any": ["error", { "ignoreRestArgs": true }] } } diff --git a/src/plugins/expressions/common/ast/build_expression.ts b/src/plugins/expressions/common/ast/build_expression.ts index 0f4618b3e699..6e84594022fd 100644 --- a/src/plugins/expressions/common/ast/build_expression.ts +++ b/src/plugins/expressions/common/ast/build_expression.ts @@ -32,13 +32,13 @@ import { parse } from './parse'; * @param val Value you want to check. * @return boolean */ -export function isExpressionAstBuilder(val: any): val is ExpressionAstExpressionBuilder { - return val?.type === 'expression_builder'; +export function isExpressionAstBuilder(val: unknown): val is ExpressionAstExpressionBuilder { + return (val as Record | undefined)?.type === 'expression_builder'; } /** @internal */ -export function isExpressionAst(val: any): val is ExpressionAstExpression { - return val?.type === 'expression'; +export function isExpressionAst(val: unknown): val is ExpressionAstExpression { + return (val as Record | undefined)?.type === 'expression'; } export interface ExpressionAstExpressionBuilder { diff --git a/src/plugins/expressions/common/ast/types.ts b/src/plugins/expressions/common/ast/types.ts index e5a79a0a5dda..8f376ac547d2 100644 --- a/src/plugins/expressions/common/ast/types.ts +++ b/src/plugins/expressions/common/ast/types.ts @@ -64,7 +64,7 @@ export type ExpressionAstFunctionDebug = { /** * Raw error that was thrown by the function, if any. */ - rawError?: any | Error; + rawError?: any | Error; // eslint-disable-line @typescript-eslint/no-explicit-any /** * Time in milliseconds it took to execute the function. Duration can be diff --git a/src/plugins/expressions/common/execution/execution.abortion.test.ts b/src/plugins/expressions/common/execution/execution.abortion.test.ts index 798558ba7ffb..fca030fb9a08 100644 --- a/src/plugins/expressions/common/execution/execution.abortion.test.ts +++ b/src/plugins/expressions/common/execution/execution.abortion.test.ts @@ -90,7 +90,7 @@ describe('Execution abortion tests', () => { const completed = jest.fn(); const aborted = jest.fn(); - const defer: ExpressionFunctionDefinition<'defer', any, { time: number }, any> = { + const defer: ExpressionFunctionDefinition<'defer', unknown, { time: number }, unknown> = { name: 'defer', args: { time: { diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index c478977f6076..9b889c62e9ff 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -17,7 +17,7 @@ import { ExecutionContract } from './execution_contract'; beforeAll(() => { if (typeof performance === 'undefined') { - (global as any).performance = { now: Date.now }; + global.performance = { now: Date.now } as typeof performance; } }); @@ -41,7 +41,7 @@ const createExecution = ( const run = async ( expression: string = 'foo bar=123', context?: Record, - input: any = null + input: unknown = null ) => { const execution = createExecution(expression, context); execution.start(input); @@ -262,45 +262,45 @@ describe('Execution', () => { describe('execution context', () => { test('context.variables is an object', async () => { - const { result } = (await run('introspectContext key="variables"')) as any; + const { result } = await run('introspectContext key="variables"'); expect(result).toHaveProperty('result', expect.any(Object)); }); test('context.types is an object', async () => { - const { result } = (await run('introspectContext key="types"')) as any; + const { result } = await run('introspectContext key="types"'); expect(result).toHaveProperty('result', expect.any(Object)); }); test('context.abortSignal is an object', async () => { - const { result } = (await run('introspectContext key="abortSignal"')) as any; + const { result } = await run('introspectContext key="abortSignal"'); expect(result).toHaveProperty('result', expect.any(Object)); }); test('context.inspectorAdapters is an object', async () => { - const { result } = (await run('introspectContext key="inspectorAdapters"')) as any; + const { result } = await run('introspectContext key="inspectorAdapters"'); expect(result).toHaveProperty('result', expect.any(Object)); }); test('context.getKibanaRequest is a function if provided', async () => { - const { result } = (await run('introspectContext key="getKibanaRequest"', { + const { result } = await run('introspectContext key="getKibanaRequest"', { kibanaRequest: {}, - })) as any; + }); expect(result).toHaveProperty('result', expect.any(Function)); }); test('context.getKibanaRequest is undefined if not provided', async () => { - const { result } = (await run('introspectContext key="getKibanaRequest"')) as any; + const { result } = await run('introspectContext key="getKibanaRequest"'); expect(result).toHaveProperty('result', undefined); }); test('unknown context key is undefined', async () => { - const { result } = (await run('introspectContext key="foo"')) as any; + const { result } = await run('introspectContext key="foo"'); expect(result).toHaveProperty('result', undefined); }); @@ -314,7 +314,7 @@ describe('Execution', () => { describe('inspector adapters', () => { test('by default, "tables" and "requests" inspector adapters are available', async () => { - const { result } = (await run('introspectContext key="inspectorAdapters"')) as any; + const { result } = await run('introspectContext key="inspectorAdapters"'); expect(result).toHaveProperty( 'result', expect.objectContaining({ @@ -326,9 +326,9 @@ describe('Execution', () => { test('can set custom inspector adapters', async () => { const inspectorAdapters = {}; - const { result } = (await run('introspectContext key="inspectorAdapters"', { + const { result } = await run('introspectContext key="inspectorAdapters"', { inspectorAdapters, - })) as any; + }); expect(result).toHaveProperty('result', inspectorAdapters); }); @@ -351,7 +351,7 @@ describe('Execution', () => { describe('expression abortion', () => { test('context has abortSignal object', async () => { - const { result } = (await run('introspectContext key="abortSignal"')) as any; + const { result } = await run('introspectContext key="abortSignal"'); expect(result).toHaveProperty('result.aborted', false); }); @@ -400,7 +400,7 @@ describe('Execution', () => { testScheduler.run(({ cold, expectObservable }) => { const arg = cold(' -a-b-c|', { a: 1, b: 2, c: 3 }); const expected = ' -a-b-c|'; - const observable: ExpressionFunctionDefinition<'observable', any, {}, any> = { + const observable: ExpressionFunctionDefinition<'observable', unknown, {}, unknown> = { name: 'observable', args: {}, help: '', @@ -468,7 +468,7 @@ describe('Execution', () => { }); test('does not execute remaining functions in pipeline', async () => { - const spy: ExpressionFunctionDefinition<'spy', any, {}, any> = { + const spy: ExpressionFunctionDefinition<'spy', unknown, {}, unknown> = { name: 'spy', args: {}, help: '', @@ -621,7 +621,12 @@ describe('Execution', () => { help: '', fn: () => arg2, }; - const max: ExpressionFunctionDefinition<'max', any, { val1: number; val2: number }, any> = { + const max: ExpressionFunctionDefinition< + 'max', + unknown, + { val1: number; val2: number }, + unknown + > = { name: 'max', args: { val1: { help: '', types: ['number'] }, @@ -679,7 +684,12 @@ describe('Execution', () => { describe('when arguments are missing', () => { it('when required argument is missing and has not alias, returns error', async () => { - const requiredArg: ExpressionFunctionDefinition<'requiredArg', any, { arg: any }, any> = { + const requiredArg: ExpressionFunctionDefinition< + 'requiredArg', + unknown, + { arg: unknown }, + unknown + > = { name: 'requiredArg', args: { arg: { diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 0bb12951202a..54a4800ec7c3 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isPromise } from '@kbn/std'; +import { ObservableLike, UnwrapObservable, UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { keys, last, mapValues, reduce, zipObject } from 'lodash'; import { combineLatest, @@ -44,6 +45,18 @@ import { ExecutionContract } from './execution_contract'; import { ExpressionExecutionParams } from '../service'; import { createDefaultInspectorAdapters } from '../util/create_default_inspector_adapters'; +type UnwrapReturnType unknown> = + ReturnType extends ObservableLike + ? UnwrapObservable> + : UnwrapPromiseOrReturn>; + +// type ArgumentsOf = Function extends ExpressionFunction< +// unknown, +// infer Arguments +// > +// ? Arguments +// : never; + /** * The result returned after an expression function execution. */ @@ -83,7 +96,7 @@ const createAbortErrorValue = () => }); export interface ExecutionParams { - executor: Executor; + executor: Executor; ast?: ExpressionAstExpression; expression?: string; params: ExpressionExecutionParams; @@ -107,7 +120,7 @@ export class Execution< * N.B. It is initialized to `null` rather than `undefined` for legacy reasons, * because in legacy interpreter it was set to `null` by default. */ - public input: Input = null as any; + public input = null as unknown as Input; /** * Input of the started execution. @@ -186,13 +199,13 @@ export class Execution< }); const inspectorAdapters = - execution.params.inspectorAdapters || createDefaultInspectorAdapters(); + (execution.params.inspectorAdapters as InspectorAdapters) || createDefaultInspectorAdapters(); this.context = { getSearchContext: () => this.execution.params.searchContext || {}, getSearchSessionId: () => execution.params.searchSessionId, getKibanaRequest: execution.params.kibanaRequest - ? () => execution.params.kibanaRequest + ? () => execution.params.kibanaRequest! : undefined, variables: execution.params.variables || {}, types: executor.getTypes(), @@ -201,14 +214,14 @@ export class Execution< logDatatable: (name: string, datatable: Datatable) => { inspectorAdapters.tables[name] = datatable; }, - isSyncColorsEnabled: () => execution.params.syncColors, - ...(execution.params as any).extraContext, + isSyncColorsEnabled: () => execution.params.syncColors!, + ...execution.params.extraContext, getExecutionContext: () => execution.params.executionContext, }; this.result = this.input$.pipe( switchMap((input) => - this.race(this.invokeChain(this.state.get().ast.chain, input)).pipe( + this.race(this.invokeChain(this.state.get().ast.chain, input)).pipe( (source) => new Observable>((subscriber) => { let latest: ExecutionResult | undefined; @@ -270,8 +283,8 @@ export class Execution< * N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons, * because in legacy interpreter it was set to `null` by default. */ - public start( - input: Input = null as any, + start( + input = null as unknown as Input, isSubExpression?: boolean ): Observable> { if (this.hasStarted) throw new Error('Execution already started.'); @@ -294,7 +307,10 @@ export class Execution< return this.result; } - invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Observable { + invokeChain( + chainArr: ExpressionAstFunction[], + input: unknown + ): Observable { return of(input).pipe( ...(chainArr.map((link) => switchMap((currentInput) => { @@ -364,19 +380,24 @@ export class Execution< }) ) as Parameters['pipe']>), catchError((error) => of(error)) - ); + ) as Observable; } - invokeFunction( - fn: ExpressionFunction, + invokeFunction( + fn: Fn, input: unknown, args: Record - ): Observable { + ): Observable> { return of(input).pipe( map((currentInput) => this.cast(currentInput, fn.inputTypes)), switchMap((normalizedInput) => this.race(of(fn.fn(normalizedInput, args, this.context)))), - switchMap((fnResult: any) => - isObservable(fnResult) ? fnResult : from(isPromise(fnResult) ? fnResult : [fnResult]) + switchMap( + (fnResult) => + (isObservable(fnResult) + ? fnResult + : from(isPromise(fnResult) ? fnResult : [fnResult])) as Observable< + UnwrapReturnType + > ), map((output) => { // Validate that the function returned the type it said it would. @@ -405,39 +426,49 @@ export class Execution< ); } - public cast(value: any, toTypeNames?: string[]) { + public cast(value: unknown, toTypeNames?: string[]): Type { // If you don't give us anything to cast to, you'll get your input back - if (!toTypeNames || toTypeNames.length === 0) return value; + if (!toTypeNames?.length) { + return value as Type; + } // No need to cast if node is already one of the valid types const fromTypeName = getType(value); - if (toTypeNames.includes(fromTypeName)) return value; + if (toTypeNames.includes(fromTypeName)) { + return value as Type; + } const { types } = this.state.get(); const fromTypeDef = types[fromTypeName]; for (const toTypeName of toTypeNames) { // First check if the current type can cast to this type - if (fromTypeDef && fromTypeDef.castsTo(toTypeName)) { + if (fromTypeDef?.castsTo(toTypeName)) { return fromTypeDef.to(value, toTypeName, types); } // If that isn't possible, check if this type can cast from the current type const toTypeDef = types[toTypeName]; - if (toTypeDef && toTypeDef.castsFrom(fromTypeName)) return toTypeDef.from(value, types); + if (toTypeDef?.castsFrom(fromTypeName)) { + return toTypeDef.from(value, types); + } } throw new Error(`Can not cast '${fromTypeName}' to any of '${toTypeNames.join(', ')}'`); } // Processes the multi-valued AST argument values into arguments that can be passed to the function - resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Observable { + resolveArgs( + fnDef: Fn, + input: unknown, + argAsts: Record + ): Observable> { return defer(() => { const { args: argDefs } = fnDef; // Use the non-alias name from the argument definition const dealiasedArgAsts = reduce( - argAsts as Record, + argAsts, (acc, argAst, argName) => { const argDef = getByAlias(argDefs, argName); if (!argDef) { @@ -452,7 +483,7 @@ export class Execution< // Check for missing required arguments. for (const { aliases, default: argDefault, name, required } of Object.values(argDefs)) { if (!(name in dealiasedArgAsts) && typeof argDefault !== 'undefined') { - dealiasedArgAsts[name] = [parse(argDefault, 'argument')]; + dealiasedArgAsts[name] = [parse(argDefault as string, 'argument')]; } if (!required || name in dealiasedArgAsts) { @@ -490,7 +521,7 @@ export class Execution< const argNames = keys(resolveArgFns); if (!argNames.length) { - return from([[]]); + return from([{}]); } const resolvedArgValuesObservable = combineLatest( @@ -523,7 +554,7 @@ export class Execution< }); } - public interpret(ast: ExpressionAstNode, input: T): Observable> { + interpret(ast: ExpressionAstNode, input: T): Observable> { switch (getType(ast)) { case 'expression': const execution = this.execution.executor.createExecution( diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index 06eac98feba6..9264891b2e0b 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -11,7 +11,7 @@ import type { SerializableRecord } from '@kbn/utility-types'; import type { KibanaRequest } from 'src/core/server'; import type { KibanaExecutionContext } from 'src/core/public'; -import { ExpressionType } from '../expression_types'; +import { Datatable, ExpressionType } from '../expression_types'; import { Adapters, RequestAdapter } from '../../../inspector/common'; import { TablesAdapter } from '../util/tables_adapter'; @@ -69,6 +69,11 @@ export interface ExecutionContext< * Contains the meta-data about the source of the expression. */ getExecutionContext: () => KibanaExecutionContext | undefined; + + /** + * Logs datatable. + */ + logDatatable?(name: string, datatable: Datatable): void; } /** diff --git a/src/plugins/expressions/common/executor/container.ts b/src/plugins/expressions/common/executor/container.ts index 9d3796ac64f4..c8e24974126f 100644 --- a/src/plugins/expressions/common/executor/container.ts +++ b/src/plugins/expressions/common/executor/container.ts @@ -19,7 +19,7 @@ export interface ExecutorState = Record< context: Context; } -export const defaultState: ExecutorState = { +export const defaultState: ExecutorState = { functions: {}, types: {}, context: {}, @@ -61,7 +61,7 @@ export type ExecutorContainer = Record = Record >( - state: ExecutorState = defaultState + state = defaultState as ExecutorState ): ExecutorContainer => { const container = createStateContainer< ExecutorState, diff --git a/src/plugins/expressions/common/executor/executor.execution.test.ts b/src/plugins/expressions/common/executor/executor.execution.test.ts index 38022c0f7dc4..ad7e6e6a014c 100644 --- a/src/plugins/expressions/common/executor/executor.execution.test.ts +++ b/src/plugins/expressions/common/executor/executor.execution.test.ts @@ -8,20 +8,14 @@ import { Executor } from './executor'; import { parseExpression } from '../ast'; +import { Execution } from '../execution/execution'; -// eslint-disable-next-line -const { __getArgs } = require('../execution/execution'); +jest.mock('../execution/execution', () => ({ + Execution: jest.fn(), +})); -jest.mock('../execution/execution', () => { - const mockedModule = { - args: undefined, - __getArgs: () => mockedModule.args, - Execution: function ExecutionMock(...args: any) { - mockedModule.args = args; - }, - }; - - return mockedModule; +beforeEach(() => { + jest.clearAllMocks(); }); describe('Executor mocked execution tests', () => { @@ -31,7 +25,9 @@ describe('Executor mocked execution tests', () => { const executor = new Executor(); executor.createExecution('foo bar="baz"'); - expect(__getArgs()[0].expression).toBe('foo bar="baz"'); + expect(Execution).toHaveBeenCalledWith( + expect.objectContaining({ expression: 'foo bar="baz"' }) + ); }); }); @@ -41,7 +37,9 @@ describe('Executor mocked execution tests', () => { const ast = parseExpression('foo bar="baz"'); executor.createExecution(ast); - expect(__getArgs()[0].expression).toBe(undefined); + expect(Execution).toHaveBeenCalledWith( + expect.not.objectContaining({ expression: expect.anything() }) + ); }); }); }); diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts index 4a3d6045a7b4..60f0f0da4e15 100644 --- a/src/plugins/expressions/common/executor/executor.test.ts +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -145,7 +145,7 @@ describe('Executor', () => { executor.extendContext({ foo }); const execution = executor.createExecution('foo bar="baz"'); - expect((execution.context as any).foo).toBe(foo); + expect(execution.context).toHaveProperty('foo', foo); }); }); }); @@ -175,10 +175,10 @@ describe('Executor', () => { migrations: { '7.10.0': ((state: ExpressionAstFunction, version: string): ExpressionAstFunction => { return migrateFn(state, version); - }) as any as MigrateFunction, + }) as unknown as MigrateFunction, '7.10.1': ((state: ExpressionAstFunction, version: string): ExpressionAstFunction => { return migrateFn(state, version); - }) as any as MigrateFunction, + }) as unknown as MigrateFunction, }, fn: jest.fn(), }; diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index ce411ea94eaf..f4913c4953ba 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -40,7 +40,7 @@ export interface ExpressionExecOptions { } export class TypesRegistry implements IRegistry { - constructor(private readonly executor: Executor) {} + constructor(private readonly executor: Executor) {} public register( typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition) @@ -62,7 +62,7 @@ export class TypesRegistry implements IRegistry { } export class FunctionsRegistry implements IRegistry { - constructor(private readonly executor: Executor) {} + constructor(private readonly executor: Executor) {} public register( functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition) @@ -100,12 +100,12 @@ export class Executor = Record; @@ -207,15 +207,15 @@ export class Executor = Record { - const executionParams: ExecutionParams = { + const executionParams = { executor: this, params: { ...params, // for canvas we are passing this in, // canvas should be refactored to not pass any extra context in extraContext: this.context, - } as any, - }; + }, + } as ExecutionParams; if (typeof ast === 'string') executionParams.expression = ast; else executionParams.ast = ast; @@ -273,7 +273,7 @@ export class Executor = Record) { + public telemetry(ast: ExpressionAstExpression, telemetryData: Record) { this.walkAst(cloneDeep(ast), (fn, link) => { telemetryData = fn.telemetry(link.arguments, telemetryData); }); diff --git a/src/plugins/expressions/common/expression_functions/arguments.ts b/src/plugins/expressions/common/expression_functions/arguments.ts index 67f83cabc645..7e39f019f00c 100644 --- a/src/plugins/expressions/common/expression_functions/arguments.ts +++ b/src/plugins/expressions/common/expression_functions/arguments.ts @@ -24,9 +24,9 @@ export type ArgumentType = * representation of the type. */ // prettier-ignore -type ArrayTypeToArgumentString = - T extends Array ? TypeString : - T extends null ? 'null' : +type ArrayTypeToArgumentString = + T extends Array ? TypeString : + T extends null ? 'null' : never; /** @@ -34,9 +34,9 @@ type ArrayTypeToArgumentString = * string-based representation of the return type. */ // prettier-ignore -type UnresolvedTypeToArgumentString = - T extends (...args: any) => infer ElementType ? TypeString : - T extends null ? 'null' : +type UnresolvedTypeToArgumentString = + T extends (...args: any[]) => infer ElementType ? TypeString : + T extends null ? 'null' : never; /** @@ -44,10 +44,10 @@ type UnresolvedTypeToArgumentString = * string-based representation of the return type. */ // prettier-ignore -type UnresolvedArrayTypeToArgumentString = - T extends Array<(...args: any) => infer ElementType> ? TypeString : - T extends (...args: any) => infer ElementType ? ArrayTypeToArgumentString : - T extends null ? 'null' : +type UnresolvedArrayTypeToArgumentString = + T extends Array<(...args: any[]) => infer ElementType> ? TypeString : + T extends (...args: any[]) => infer ElementType ? ArrayTypeToArgumentString : + T extends null ? 'null' : never; /** A type containing properties common to all Function Arguments. */ diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts index 05a5dbb638c0..8154534b32ab 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function.ts @@ -36,7 +36,11 @@ export class ExpressionFunction implements PersistableState, handlers: object) => ExpressionValue; + fn: ( + input: ExpressionValue, + params: Record, + handlers: object + ) => ExpressionValue; /** * A short help text. @@ -56,8 +60,8 @@ export class ExpressionFunction implements PersistableState - ) => Record; + telemetryData: Record + ) => Record; extract: (state: ExpressionAstFunction['arguments']) => { state: ExpressionAstFunction['arguments']; references: SavedObjectReference[]; @@ -100,13 +104,12 @@ export class ExpressionFunction implements PersistableState { // If you don't tell us input types, we'll assume you don't care what you get. - if (!this.inputTypes) return true; - return this.inputTypes.indexOf(type) > -1; + return this.inputTypes?.includes(type) ?? true; }; } diff --git a/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts b/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts index bfc45d65f1c9..9942c9af7ff7 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts @@ -6,20 +6,21 @@ * Side Public License, v 1. */ +import { KnownTypeToString } from '../types'; import { ArgumentType } from './arguments'; -export class ExpressionFunctionParameter { +export class ExpressionFunctionParameter { name: string; required: boolean; help: string; - types: string[]; - default: any; + types: ArgumentType['types']; + default?: ArgumentType['default']; aliases: string[]; multi: boolean; resolve: boolean; - options: any[]; + options: T[]; - constructor(name: string, arg: ArgumentType) { + constructor(name: string, arg: ArgumentType) { const { required, help, types, aliases, multi, resolve, options } = arg; if (name === '_') { @@ -38,7 +39,6 @@ export class ExpressionFunctionParameter { } accepts(type: string) { - if (!this.types.length) return true; - return this.types.indexOf(type) > -1; + return !this.types?.length || this.types.includes(type as KnownTypeToString); } } diff --git a/src/plugins/expressions/common/expression_functions/expression_function_parameters.test.ts b/src/plugins/expressions/common/expression_functions/expression_function_parameters.test.ts index 6e47634f5aac..28ef5243e0fe 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function_parameters.test.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function_parameters.test.ts @@ -21,7 +21,7 @@ describe('ExpressionFunctionParameter', () => { const param = new ExpressionFunctionParameter('foo', { help: 'bar', types: ['baz', 'quux'], - }); + } as ConstructorParameters[1]); expect(param.accepts('baz')).toBe(true); expect(param.accepts('quux')).toBe(true); diff --git a/src/plugins/expressions/common/expression_functions/specs/create_table.ts b/src/plugins/expressions/common/expression_functions/specs/create_table.ts index 5174b258a4d9..0ce427e817e2 100644 --- a/src/plugins/expressions/common/expression_functions/specs/create_table.ts +++ b/src/plugins/expressions/common/expression_functions/specs/create_table.ts @@ -11,9 +11,9 @@ import { ExpressionFunctionDefinition } from '../types'; import { Datatable, DatatableColumn } from '../../expression_types'; export interface CreateTableArguments { - ids: string[]; - names: string[] | null; - rowCount: number; + ids?: string[]; + names?: string[] | null; + rowCount?: number; } export const createTable: ExpressionFunctionDefinition< diff --git a/src/plugins/expressions/common/expression_functions/specs/font.ts b/src/plugins/expressions/common/expression_functions/specs/font.ts index fa8fc8d387af..3d189a68119d 100644 --- a/src/plugins/expressions/common/expression_functions/specs/font.ts +++ b/src/plugins/expressions/common/expression_functions/specs/font.ts @@ -30,7 +30,7 @@ const inlineStyle = (obj: Record) => { return styles.join(';'); }; -interface Arguments { +export interface FontArguments { align?: TextAlignment; color?: string; family?: FontFamily; @@ -41,7 +41,12 @@ interface Arguments { weight?: FontWeight; } -export type ExpressionFunctionFont = ExpressionFunctionDefinition<'font', null, Arguments, Style>; +export type ExpressionFunctionFont = ExpressionFunctionDefinition< + 'font', + null, + FontArguments, + Style +>; export const font: ExpressionFunctionFont = { name: 'font', diff --git a/src/plugins/expressions/common/expression_functions/specs/map_column.ts b/src/plugins/expressions/common/expression_functions/specs/map_column.ts index 23aeee6f9581..7b2266637bfb 100644 --- a/src/plugins/expressions/common/expression_functions/specs/map_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/map_column.ts @@ -110,7 +110,7 @@ export const mapColumn: ExpressionFunctionDefinition< map((rows) => { let type: DatatableColumnType = 'null'; for (const row of rows) { - const rowType = getType(row[id]); + const rowType = getType(row[id]) as DatatableColumnType; if (rowType !== 'null') { type = rowType; break; diff --git a/src/plugins/expressions/common/expression_functions/specs/math.ts b/src/plugins/expressions/common/expression_functions/specs/math.ts index 92a10976428a..f843f53e4dd8 100644 --- a/src/plugins/expressions/common/expression_functions/specs/math.ts +++ b/src/plugins/expressions/common/expression_functions/specs/math.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { map, zipObject } from 'lodash'; +import { map, zipObject, isString } from 'lodash'; import { i18n } from '@kbn/i18n'; import { evaluate } from '@kbn/tinymath'; import { ExpressionFunctionDefinition } from '../types'; @@ -23,19 +23,18 @@ const TINYMATH = '`TinyMath`'; const TINYMATH_URL = 'https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html'; -const isString = (val: any): boolean => typeof val === 'string'; - function pivotObjectArray< - RowType extends { [key: string]: any }, - ReturnColumns extends string | number | symbol = keyof RowType ->(rows: RowType[], columns?: string[]): Record { + RowType extends { [key: string]: unknown }, + ReturnColumns extends keyof RowType & string +>(rows: RowType[], columns?: ReturnColumns[]) { const columnNames = columns || Object.keys(rows[0]); if (!columnNames.every(isString)) { throw new Error('Columns should be an array of strings'); } const columnValues = map(columnNames, (name) => map(rows, name)); - return zipObject(columnNames, columnValues); + + return zipObject(columnNames, columnValues) as { [K in ReturnColumns]: Array }; } export const errors = { diff --git a/src/plugins/expressions/common/expression_functions/specs/math_column.ts b/src/plugins/expressions/common/expression_functions/specs/math_column.ts index c59016cd260a..a2a79ef3f028 100644 --- a/src/plugins/expressions/common/expression_functions/specs/math_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/math_column.ts @@ -107,7 +107,7 @@ export const mathColumn: ExpressionFunctionDefinition< let type: DatatableColumnType = 'null'; if (newRows.length) { for (const row of newRows) { - const rowType = getType(row[args.id]); + const rowType = getType(row[args.id]) as DatatableColumnType; if (rowType !== 'null') { type = rowType; break; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/font.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/font.test.ts index 03b610126660..e095f4e1bec6 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/font.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/font.test.ts @@ -7,7 +7,8 @@ */ import { openSans } from '../../../fonts'; -import { font } from '../font'; +import { FontWeight, TextAlignment } from '../../../types'; +import { font, FontArguments } from '../font'; import { functionWrapper } from './utils'; describe('font', () => { @@ -22,7 +23,7 @@ describe('font', () => { size: 14, underline: false, weight: 'normal', - }; + } as unknown as FontArguments; describe('default output', () => { const result = fn(null, args); @@ -63,7 +64,7 @@ describe('font', () => { describe('family', () => { it('sets font family', () => { - const result = fn(null, { ...args, family: 'Optima, serif' }); + const result = fn(null, { ...args, family: 'Optima, serif' } as unknown as FontArguments); expect(result.spec.fontFamily).toBe('Optima, serif'); expect(result.css).toContain('font-family:Optima, serif'); }); @@ -79,29 +80,29 @@ describe('font', () => { describe('weight', () => { it('sets font weight', () => { - let result = fn(null, { ...args, weight: 'normal' }); + let result = fn(null, { ...args, weight: FontWeight.NORMAL }); expect(result.spec.fontWeight).toBe('normal'); expect(result.css).toContain('font-weight:normal'); - result = fn(null, { ...args, weight: 'bold' }); + result = fn(null, { ...args, weight: FontWeight.BOLD }); expect(result.spec.fontWeight).toBe('bold'); expect(result.css).toContain('font-weight:bold'); - result = fn(null, { ...args, weight: 'bolder' }); + result = fn(null, { ...args, weight: FontWeight.BOLDER }); expect(result.spec.fontWeight).toBe('bolder'); expect(result.css).toContain('font-weight:bolder'); - result = fn(null, { ...args, weight: 'lighter' }); + result = fn(null, { ...args, weight: FontWeight.LIGHTER }); expect(result.spec.fontWeight).toBe('lighter'); expect(result.css).toContain('font-weight:lighter'); - result = fn(null, { ...args, weight: '400' }); + result = fn(null, { ...args, weight: FontWeight.FOUR }); expect(result.spec.fontWeight).toBe('400'); expect(result.css).toContain('font-weight:400'); }); it('throws when provided an invalid weight', () => { - expect(() => fn(null, { ...args, weight: 'foo' })).toThrow(); + expect(() => fn(null, { ...args, weight: 'foo' as FontWeight })).toThrow(); }); }); @@ -131,25 +132,25 @@ describe('font', () => { describe('align', () => { it('sets text alignment', () => { - let result = fn(null, { ...args, align: 'left' }); + let result = fn(null, { ...args, align: TextAlignment.LEFT }); expect(result.spec.textAlign).toBe('left'); expect(result.css).toContain('text-align:left'); - result = fn(null, { ...args, align: 'center' }); + result = fn(null, { ...args, align: TextAlignment.CENTER }); expect(result.spec.textAlign).toBe('center'); expect(result.css).toContain('text-align:center'); - result = fn(null, { ...args, align: 'right' }); + result = fn(null, { ...args, align: TextAlignment.RIGHT }); expect(result.spec.textAlign).toBe('right'); expect(result.css).toContain('text-align:right'); - result = fn(null, { ...args, align: 'justify' }); + result = fn(null, { ...args, align: TextAlignment.JUSTIFY }); expect(result.spec.textAlign).toBe('justify'); expect(result.css).toContain('text-align:justify'); }); it('throws when provided an invalid alignment', () => { - expect(() => fn(null, { ...args, align: 'foo' })).toThrow(); + expect(() => fn(null, { ...args, align: 'foo' as TextAlignment })).toThrow(); }); }); }); diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/math.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/math.test.ts index 6da00061244d..3761fe0a4f90 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/math.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/math.test.ts @@ -6,19 +6,19 @@ * Side Public License, v 1. */ -import { errors, math } from '../math'; +import { errors, math, MathArguments, MathInput } from '../math'; import { emptyTable, functionWrapper, testTable } from './utils'; describe('math', () => { - const fn = functionWrapper(math); + const fn = functionWrapper(math); it('evaluates math expressions without reference to context', () => { - expect(fn(null, { expression: '10.5345' })).toBe(10.5345); - expect(fn(null, { expression: '123 + 456' })).toBe(579); - expect(fn(null, { expression: '100 - 46' })).toBe(54); + expect(fn(null as unknown as MathInput, { expression: '10.5345' })).toBe(10.5345); + expect(fn(null as unknown as MathInput, { expression: '123 + 456' })).toBe(579); + expect(fn(null as unknown as MathInput, { expression: '100 - 46' })).toBe(54); expect(fn(1, { expression: '100 / 5' })).toBe(20); - expect(fn('foo', { expression: '100 / 5' })).toBe(20); - expect(fn(true, { expression: '100 / 5' })).toBe(20); + expect(fn('foo' as unknown as MathInput, { expression: '100 / 5' })).toBe(20); + expect(fn(true as unknown as MathInput, { expression: '100 / 5' })).toBe(20); expect(fn(testTable, { expression: '100 * 5' })).toBe(500); expect(fn(emptyTable, { expression: '100 * 5' })).toBe(500); }); @@ -54,7 +54,7 @@ describe('math', () => { describe('args', () => { describe('expression', () => { it('sets the math expression to be evaluted', () => { - expect(fn(null, { expression: '10' })).toBe(10); + expect(fn(null as unknown as MathInput, { expression: '10' })).toBe(10); expect(fn(23.23, { expression: 'floor(value)' })).toBe(23); expect(fn(testTable, { expression: 'count(price)' })).toBe(9); expect(fn(testTable, { expression: 'count(name)' })).toBe(9); @@ -99,11 +99,11 @@ describe('math', () => { it('throws when missing expression', () => { expect(() => fn(testTable)).toThrow(new RegExp(errors.emptyExpression().message)); - expect(() => fn(testTable, { expession: '' })).toThrow( + expect(() => fn(testTable, { expession: '' } as unknown as MathArguments)).toThrow( new RegExp(errors.emptyExpression().message) ); - expect(() => fn(testTable, { expession: ' ' })).toThrow( + expect(() => fn(testTable, { expession: ' ' } as unknown as MathArguments)).toThrow( new RegExp(errors.emptyExpression().message) ); }); diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/theme.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/theme.test.ts index 0733b1c77bf4..3f535b7fb7ac 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/theme.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/theme.test.ts @@ -26,14 +26,14 @@ describe('expression_functions', () => { }; context = { - getSearchContext: () => ({} as any), + getSearchContext: () => ({}), getSearchSessionId: () => undefined, getExecutionContext: () => undefined, types: {}, variables: { theme: themeProps }, - abortSignal: {} as any, - inspectorAdapters: {} as any, - }; + abortSignal: {}, + inspectorAdapters: {}, + } as unknown as typeof context; }); it('returns the selected variable', () => { diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/ui_setting.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/ui_setting.test.ts index 6b3a458aa7e5..053f97ffc8fb 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/ui_setting.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/ui_setting.test.ts @@ -10,13 +10,15 @@ jest.mock('../../../../common'); import { IUiSettingsClient } from 'src/core/public'; import { getUiSettingFn } from '../ui_setting'; +import { functionWrapper } from './utils'; describe('uiSetting', () => { describe('fn', () => { let getStartDependencies: jest.MockedFunction< Parameters[0]['getStartDependencies'] >; - let uiSetting: ReturnType; + const uiSettingWrapper = () => functionWrapper(getUiSettingFn({ getStartDependencies })); + let uiSetting: ReturnType; let uiSettings: jest.Mocked; beforeEach(() => { @@ -27,13 +29,13 @@ describe('uiSetting', () => { uiSettings, })) as unknown as typeof getStartDependencies; - uiSetting = getUiSettingFn({ getStartDependencies }); + uiSetting = uiSettingWrapper(); }); it('should return a value', () => { uiSettings.get.mockReturnValueOnce('value'); - expect(uiSetting.fn(null, { parameter: 'something' }, {} as any)).resolves.toEqual({ + expect(uiSetting(null, { parameter: 'something' })).resolves.toEqual({ type: 'ui_setting', key: 'something', value: 'value', @@ -41,7 +43,7 @@ describe('uiSetting', () => { }); it('should pass a default value', async () => { - await uiSetting.fn(null, { parameter: 'something', default: 'default' }, {} as any); + await uiSetting(null, { parameter: 'something', default: 'default' }); expect(uiSettings.get).toHaveBeenCalledWith('something', 'default'); }); @@ -51,16 +53,16 @@ describe('uiSetting', () => { throw new Error(); }); - expect(uiSetting.fn(null, { parameter: 'something' }, {} as any)).rejects.toEqual( + expect(uiSetting(null, { parameter: 'something' })).rejects.toEqual( new Error('Invalid parameter "something".') ); }); it('should get a request instance on the server-side', async () => { const request = {}; - await uiSetting.fn(null, { parameter: 'something' }, { + await uiSetting(null, { parameter: 'something' }, { getKibanaRequest: () => request, - } as any); + } as Parameters[2]); const [[getKibanaRequest]] = getStartDependencies.mock.calls; @@ -68,7 +70,7 @@ describe('uiSetting', () => { }); it('should throw an error if request is not provided on the server-side', async () => { - await uiSetting.fn(null, { parameter: 'something' }, {} as any); + await uiSetting(null, { parameter: 'something' }); const [[getKibanaRequest]] = getStartDependencies.mock.calls; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/utils.ts b/src/plugins/expressions/common/expression_functions/specs/tests/utils.ts index ca41b427a28f..e3f581d1ae35 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/utils.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/utils.ts @@ -15,13 +15,15 @@ import { Datatable } from '../../../expression_types'; * Takes a function spec and passes in default args, * overriding with any provided args. */ -export const functionWrapper = ( - spec: AnyExpressionFunctionDefinition +export const functionWrapper = < + ExpressionFunctionDefinition extends AnyExpressionFunctionDefinition +>( + spec: ExpressionFunctionDefinition ) => { const defaultArgs = mapValues(spec.args, (argSpec) => argSpec.default); return ( - context: ContextType, - args: Record = {}, + context?: Parameters[0] | null, + args: Parameters[1] = {}, handlers: ExecutionContext = {} as ExecutionContext ) => spec.fn(context, { ...defaultArgs, ...args }, handlers); }; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts index 0c5c6b148020..ca9c9c257f70 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts @@ -24,9 +24,9 @@ describe('expression_functions', () => { getExecutionContext: () => undefined, types: {}, variables: { test: 1 }, - abortSignal: {} as any, - inspectorAdapters: {} as any, - }; + abortSignal: {}, + inspectorAdapters: {}, + } as unknown as typeof context; }); it('returns the selected variable', () => { diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts index 95287f71907a..b98e8285a1a8 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts @@ -17,7 +17,7 @@ describe('expression_functions', () => { const fn = functionWrapper(variableSet); let input: Partial>; let context: ExecutionContext; - let variables: Record; + let variables: Record; beforeEach(() => { input = { timeRange: { from: '0', to: '1' } }; @@ -27,9 +27,9 @@ describe('expression_functions', () => { getExecutionContext: () => undefined, types: {}, variables: { test: 1 }, - abortSignal: {} as any, - inspectorAdapters: {} as any, - }; + abortSignal: {}, + inspectorAdapters: {}, + } as unknown as typeof context; variables = context.variables; }); diff --git a/src/plugins/expressions/common/expression_functions/specs/theme.ts b/src/plugins/expressions/common/expression_functions/specs/theme.ts index 914e5d330bdd..76e97b12a967 100644 --- a/src/plugins/expressions/common/expression_functions/specs/theme.ts +++ b/src/plugins/expressions/common/expression_functions/specs/theme.ts @@ -12,10 +12,10 @@ import { ExpressionFunctionDefinition } from '../types'; interface Arguments { variable: string; - default: string | number | boolean; + default?: string | number | boolean; } -type Output = any; +type Output = unknown; export type ExpressionFunctionTheme = ExpressionFunctionDefinition< 'theme', diff --git a/src/plugins/expressions/common/expression_functions/specs/var.ts b/src/plugins/expressions/common/expression_functions/specs/var.ts index d0e43a687147..0ba4d5f43935 100644 --- a/src/plugins/expressions/common/expression_functions/specs/var.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var.ts @@ -36,7 +36,8 @@ export const variable: ExpressionFunctionVar = { }, }, fn(input, args, context) { - const variables: Record = context.variables; + const { variables } = context; + return variables[args.name]; }, }; diff --git a/src/plugins/expressions/common/expression_functions/specs/var_set.ts b/src/plugins/expressions/common/expression_functions/specs/var_set.ts index f3ac6a2ab80d..aa257940f9ad 100644 --- a/src/plugins/expressions/common/expression_functions/specs/var_set.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var_set.ts @@ -7,11 +7,12 @@ */ import { i18n } from '@kbn/i18n'; +import type { Serializable } from '@kbn/utility-types'; import { ExpressionFunctionDefinition } from '../types'; interface Arguments { name: string[]; - value: any[]; + value: Serializable[]; } export type ExpressionFunctionVarSet = ExpressionFunctionDefinition< @@ -46,10 +47,11 @@ export const variableSet: ExpressionFunctionVarSet = { }, }, fn(input, args, context) { - const variables: Record = context.variables; + const { variables } = context; args.name.forEach((name, i) => { variables[name] = args.value[i] === undefined ? input : args.value[i]; }); + return input; }, }; diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index 0ec61b39608a..cb3677ed1668 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -30,7 +30,7 @@ import { PersistableStateDefinition } from '../../../kibana_utils/common'; export interface ExpressionFunctionDefinition< Name extends string, Input, - Arguments extends Record, + Arguments extends Record, Output, Context extends ExecutionContext = ExecutionContext > extends PersistableStateDefinition { @@ -99,12 +99,14 @@ export interface ExpressionFunctionDefinition< /** * Type to capture every possible expression function definition. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ export type AnyExpressionFunctionDefinition = ExpressionFunctionDefinition< string, any, Record, any >; +/* eslint-enable @typescript-eslint/no-explicit-any */ /** * A mapping of `ExpressionFunctionDefinition`s for functions which the diff --git a/src/plugins/expressions/common/expression_renderers/types.ts b/src/plugins/expressions/common/expression_renderers/types.ts index 8547c1a1bec9..6c889a81a1f8 100644 --- a/src/plugins/expressions/common/expression_renderers/types.ts +++ b/src/plugins/expressions/common/expression_renderers/types.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { ExpressionAstExpression } from '../ast'; + export interface ExpressionRenderDefinition { /** * Technical name of the renderer, used as ID to identify renderer in @@ -46,6 +48,7 @@ export interface ExpressionRenderDefinition { ) => void | Promise; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type AnyExpressionRenderDefinition = ExpressionRenderDefinition; /** @@ -59,24 +62,34 @@ export type AnyExpressionRenderDefinition = ExpressionRenderDefinition; */ export type RenderMode = 'edit' | 'preview' | 'view'; +export interface IInterpreterRenderUpdateParams { + newExpression?: string | ExpressionAstExpression; + newParams: Params; +} + +export interface IInterpreterRenderEvent { + name: string; + data?: Context; +} + export interface IInterpreterRenderHandlers { /** * Done increments the number of rendering successes */ - done: () => void; - onDestroy: (fn: () => void) => void; - reload: () => void; - update: (params: any) => void; - event: (event: any) => void; - hasCompatibleActions?: (event: any) => Promise; - getRenderMode: () => RenderMode; + done(): void; + onDestroy(fn: () => void): void; + reload(): void; + update(params: IInterpreterRenderUpdateParams): void; + event(event: IInterpreterRenderEvent): void; + hasCompatibleActions?(event: IInterpreterRenderEvent): Promise; + getRenderMode(): RenderMode; /** * The chart is rendered in a non-interactive environment and should not provide any affordances for interaction like brushing. */ - isInteractive: () => boolean; + isInteractive(): boolean; - isSyncColorsEnabled: () => boolean; + isSyncColorsEnabled(): boolean; /** * This uiState interface is actually `PersistedState` from the visualizations plugin, * but expressions cannot know about vis or it creates a mess of circular dependencies. diff --git a/src/plugins/expressions/common/expression_types/expression_type.ts b/src/plugins/expressions/common/expression_types/expression_type.ts index 1c22b9f13b97..d179beeb7686 100644 --- a/src/plugins/expressions/common/expression_types/expression_type.ts +++ b/src/plugins/expressions/common/expression_types/expression_type.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { Serializable } from '@kbn/utility-types'; import { AnyExpressionTypeDefinition, ExpressionValue, ExpressionValueConverter } from './types'; import { getType } from './get_type'; @@ -20,15 +21,15 @@ export class ExpressionType { /** * Type validation, useful for checking function output. */ - validate: (type: any) => void | Error; + validate: (type: unknown) => void | Error; create: unknown; /** * Optional serialization (used when passing context around client/server). */ - serialize?: (value: ExpressionValue) => any; - deserialize?: (serialized: any) => ExpressionValue; + serialize?: (value: Serializable) => unknown; + deserialize?: (serialized: unknown[]) => Serializable; constructor(private readonly definition: AnyExpressionTypeDefinition) { const { name, help, deserialize, serialize, validate } = definition; @@ -38,7 +39,7 @@ export class ExpressionType { this.validate = validate || (() => {}); // Optional - this.create = (definition as any).create; + this.create = (definition as unknown as Record<'create', unknown>).create; this.serialize = serialize; this.deserialize = deserialize; diff --git a/src/plugins/expressions/common/expression_types/get_type.ts b/src/plugins/expressions/common/expression_types/get_type.ts index 052508df4132..17089c78816b 100644 --- a/src/plugins/expressions/common/expression_types/get_type.ts +++ b/src/plugins/expressions/common/expression_types/get_type.ts @@ -6,14 +6,24 @@ * Side Public License, v 1. */ -export function getType(node: any) { - if (node == null) return 'null'; +export function getType(node: unknown): string { + if (node == null) { + return 'null'; + } + if (Array.isArray(node)) { throw new Error('Unexpected array value encountered.'); } - if (typeof node === 'object') { - if (!node.type) throw new Error('Objects must have a type property'); - return node.type; + + if (typeof node !== 'object') { + return typeof node; } - return typeof node; + + const { type } = node as Record; + + if (!type) { + throw new Error('Objects must have a type property'); + } + + return type as string; } diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index c268557936ac..b45c36950f87 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -9,7 +9,7 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { map, pick, zipObject } from 'lodash'; -import { ExpressionTypeDefinition } from '../types'; +import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; import { PointSeries, PointSeriesColumn } from './pointseries'; import { ExpressionValueRender } from './render'; import { SerializedFieldFormat } from '../../types'; @@ -21,7 +21,7 @@ const name = 'datatable'; * @param datatable */ export const isDatatable = (datatable: unknown): datatable is Datatable => - !!datatable && typeof datatable === 'object' && (datatable as any).type === 'datatable'; + (datatable as ExpressionValueBoxed | undefined)?.type === 'datatable'; /** * This type represents the `type` of any `DatatableColumn` in a `Datatable`. @@ -48,6 +48,7 @@ export type DatatableColumnType = /** * This type represents a row in a `Datatable`. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type DatatableRow = Record; /** @@ -112,7 +113,7 @@ interface RenderedDatatable { export const datatable: ExpressionTypeDefinition = { name, - validate: (table) => { + validate: (table: Record) => { // TODO: Check columns types. Only string, boolean, number, date, allowed for now. if (!table.columns) { throw new Error('datatable must have a columns array, even if it is empty'); diff --git a/src/plugins/expressions/common/expression_types/specs/error.ts b/src/plugins/expressions/common/expression_types/specs/error.ts index 75e49633866f..b170675b8f48 100644 --- a/src/plugins/expressions/common/expression_types/specs/error.ts +++ b/src/plugins/expressions/common/expression_types/specs/error.ts @@ -22,7 +22,7 @@ export type ExpressionValueError = ExpressionValueBoxed< } >; -export const isExpressionValueError = (value: any): value is ExpressionValueError => +export const isExpressionValueError = (value: unknown): value is ExpressionValueError => getType(value) === 'error'; /** diff --git a/src/plugins/expressions/common/expression_types/specs/pointseries.ts b/src/plugins/expressions/common/expression_types/specs/pointseries.ts index 1c8bddf14cff..ef2079bd387a 100644 --- a/src/plugins/expressions/common/expression_types/specs/pointseries.ts +++ b/src/plugins/expressions/common/expression_types/specs/pointseries.ts @@ -7,7 +7,7 @@ */ import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; -import { Datatable } from './datatable'; +import { Datatable, DatatableRow } from './datatable'; import { ExpressionValueRender } from './render'; const name = 'pointseries'; @@ -31,7 +31,7 @@ export interface PointSeriesColumn { */ export type PointSeriesColumns = Record | {}; -export type PointSeriesRow = Record; +export type PointSeriesRow = DatatableRow; /** * A `PointSeries` is a unique structure that represents dots on a chart. diff --git a/src/plugins/expressions/common/expression_types/specs/shape.ts b/src/plugins/expressions/common/expression_types/specs/shape.ts index 2b62dc6458c1..5ad86366a26c 100644 --- a/src/plugins/expressions/common/expression_types/specs/shape.ts +++ b/src/plugins/expressions/common/expression_types/specs/shape.ts @@ -11,7 +11,7 @@ import { ExpressionValueRender } from './render'; const name = 'shape'; -export const shape: ExpressionTypeDefinition> = { +export const shape: ExpressionTypeDefinition> = { name: 'shape', to: { render: (input) => { diff --git a/src/plugins/expressions/common/expression_types/types.ts b/src/plugins/expressions/common/expression_types/types.ts index a829c2adc923..15ec82e40314 100644 --- a/src/plugins/expressions/common/expression_types/types.ts +++ b/src/plugins/expressions/common/expression_types/types.ts @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +import type { ExpressionType } from './expression_type'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type ExpressionValueUnboxed = any; export type ExpressionValueBoxed = { @@ -16,7 +19,7 @@ export type ExpressionValue = ExpressionValueUnboxed | ExpressionValueBoxed; export type ExpressionValueConverter = ( input: I, - availableTypes: Record + availableTypes: Record ) => O; /** @@ -29,18 +32,19 @@ export interface ExpressionTypeDefinition< SerializedType = undefined > { name: Name; - validate?: (type: any) => void | Error; - serialize?: (type: Value) => SerializedType; - deserialize?: (type: SerializedType) => Value; + validate?(type: unknown): void | Error; + serialize?(type: Value): SerializedType; + deserialize?(type: SerializedType): Value; // TODO: Update typings for the `availableTypes` parameter once interfaces for this // have been added elsewhere in the interpreter. from?: { - [type: string]: ExpressionValueConverter; + [type: string]: ExpressionValueConverter; }; to?: { - [type: string]: ExpressionValueConverter; + [type: string]: ExpressionValueConverter; }; help?: string; } -export type AnyExpressionTypeDefinition = ExpressionTypeDefinition; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyExpressionTypeDefinition = ExpressionTypeDefinition; diff --git a/src/plugins/expressions/common/mocks.ts b/src/plugins/expressions/common/mocks.ts index 681fa2826882..4141da06ec04 100644 --- a/src/plugins/expressions/common/mocks.ts +++ b/src/plugins/expressions/common/mocks.ts @@ -11,7 +11,7 @@ import { ExecutionContext } from './execution/types'; export const createMockExecutionContext = ( extraContext: ExtraContext = {} as ExtraContext ): ExecutionContext & ExtraContext => { - const executionContext: ExecutionContext = { + const executionContext = { getSearchContext: jest.fn(), getSearchSessionId: jest.fn(), getExecutionContext: jest.fn(), @@ -25,10 +25,10 @@ export const createMockExecutionContext = removeEventListener: jest.fn(), }, inspectorAdapters: { - requests: {} as any, - data: {} as any, + requests: {}, + data: {}, }, - }; + } as unknown as ExecutionContext; return { ...executionContext, diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index f21eaa34d786..453ea656ec43 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -125,7 +125,7 @@ export interface ExpressionsServiceSetup { export interface ExpressionExecutionParams { searchContext?: SerializableRecord; - variables?: Record; + variables?: Record; /** * Whether to execute expression in *debug mode*. In *debug mode* inputs and @@ -148,6 +148,8 @@ export interface ExpressionExecutionParams { inspectorAdapters?: Adapters; executionContext?: KibanaExecutionContext; + + extraContext?: object; } /** @@ -375,7 +377,7 @@ export class ExpressionsService */ public readonly telemetry = ( state: ExpressionAstExpression, - telemetryData: Record = {} + telemetryData: Record = {} ) => { return this.executor.telemetry(state, telemetryData); }; diff --git a/src/plugins/expressions/common/test_helpers/expression_functions/access.ts b/src/plugins/expressions/common/test_helpers/expression_functions/access.ts index 5498e6741bd9..6ddc99b13ee5 100644 --- a/src/plugins/expressions/common/test_helpers/expression_functions/access.ts +++ b/src/plugins/expressions/common/test_helpers/expression_functions/access.ts @@ -8,7 +8,7 @@ import { ExpressionFunctionDefinition } from '../../expression_functions'; -export const access: ExpressionFunctionDefinition<'access', any, { key: string }, any> = { +export const access: ExpressionFunctionDefinition<'access', unknown, { key: string }, unknown> = { name: 'access', help: 'Access key on input object or return the input, if it is not an object', args: { @@ -19,6 +19,10 @@ export const access: ExpressionFunctionDefinition<'access', any, { key: string } }, }, fn: (input, { key }, context) => { - return !input ? input : typeof input === 'object' ? input[key] : input; + return !input + ? input + : typeof input === 'object' + ? (input as Record)[key] + : input; }, }; diff --git a/src/plugins/expressions/common/test_helpers/expression_functions/add.ts b/src/plugins/expressions/common/test_helpers/expression_functions/add.ts index 03c72733166b..5e646ead7b62 100644 --- a/src/plugins/expressions/common/test_helpers/expression_functions/add.ts +++ b/src/plugins/expressions/common/test_helpers/expression_functions/add.ts @@ -26,11 +26,11 @@ export const add: ExpressionFunctionDefinition< types: ['null', 'number', 'string'], }, }, - fn: ({ value: value1 }, { val: input2 }, context) => { + fn: ({ value: value1 }, { val: input2 }) => { const value2 = !input2 ? 0 : typeof input2 === 'object' - ? (input2 as any).value + ? (input2 as ExpressionValueNum).value : Number(input2); return { diff --git a/src/plugins/expressions/common/test_helpers/expression_functions/introspect_context.ts b/src/plugins/expressions/common/test_helpers/expression_functions/introspect_context.ts index 9556872d5734..8b2c22df8e08 100644 --- a/src/plugins/expressions/common/test_helpers/expression_functions/introspect_context.ts +++ b/src/plugins/expressions/common/test_helpers/expression_functions/introspect_context.ts @@ -10,9 +10,9 @@ import { ExpressionFunctionDefinition } from '../../expression_functions'; export const introspectContext: ExpressionFunctionDefinition< 'introspectContext', - any, + unknown, { key: string }, - any + unknown > = { name: 'introspectContext', args: { @@ -25,7 +25,7 @@ export const introspectContext: ExpressionFunctionDefinition< fn: (input, args, context) => { return { type: 'any', - result: (context as any)[args.key], + result: context[args.key as keyof typeof context], }; }, }; diff --git a/src/plugins/expressions/common/test_helpers/expression_functions/sleep.ts b/src/plugins/expressions/common/test_helpers/expression_functions/sleep.ts index 04b1c9822a3b..71a14b3dac10 100644 --- a/src/plugins/expressions/common/test_helpers/expression_functions/sleep.ts +++ b/src/plugins/expressions/common/test_helpers/expression_functions/sleep.ts @@ -8,7 +8,7 @@ import { ExpressionFunctionDefinition } from '../../expression_functions'; -export const sleep: ExpressionFunctionDefinition<'sleep', any, { time: number }, any> = { +export const sleep: ExpressionFunctionDefinition<'sleep', unknown, { time: number }, unknown> = { name: 'sleep', args: { time: { diff --git a/src/plugins/expressions/common/types/common.ts b/src/plugins/expressions/common/types/common.ts index d8d1a9a4b256..64b3d00895f5 100644 --- a/src/plugins/expressions/common/types/common.ts +++ b/src/plugins/expressions/common/types/common.ts @@ -37,7 +37,7 @@ export type KnownTypeToString = * `someArgument: Promise` results in `types: ['boolean', 'string']` */ export type TypeString = KnownTypeToString< - T extends ObservableLike ? UnwrapObservable : UnwrapPromiseOrReturn + T extends ObservableLike ? UnwrapObservable : UnwrapPromiseOrReturn >; /** @@ -52,6 +52,7 @@ export type UnmappedTypeStrings = 'date' | 'filter'; * Is used to carry information about how to format data in * a data table as part of the column definition. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export interface SerializedFieldFormat> { id?: string; params?: TParams; diff --git a/src/plugins/expressions/common/util/expressions_inspector_adapter.ts b/src/plugins/expressions/common/util/expressions_inspector_adapter.ts index 34fa1c8713f5..bf16635792c1 100644 --- a/src/plugins/expressions/common/util/expressions_inspector_adapter.ts +++ b/src/plugins/expressions/common/util/expressions_inspector_adapter.ts @@ -7,16 +7,17 @@ */ import { EventEmitter } from 'events'; +import { ExpressionAstNode } from '..'; export class ExpressionsInspectorAdapter extends EventEmitter { - private _ast: any = {}; + private _ast = {} as ExpressionAstNode; - public logAST(ast: any): void { + logAST(ast: ExpressionAstNode): void { this._ast = ast; this.emit('change', this._ast); } - public get ast() { + public get ast(): ExpressionAstNode { return this._ast; } } diff --git a/src/plugins/expressions/common/util/test_utils.ts b/src/plugins/expressions/common/util/test_utils.ts index 59bd0a4235d9..a6a4771bdf89 100644 --- a/src/plugins/expressions/common/util/test_utils.ts +++ b/src/plugins/expressions/common/util/test_utils.ts @@ -14,7 +14,7 @@ export const createMockContext = () => { getSearchSessionId: () => undefined, types: {}, variables: {}, - abortSignal: {} as any, - inspectorAdapters: {} as any, + abortSignal: {}, + inspectorAdapters: {}, } as ExecutionContext; }; diff --git a/src/plugins/expressions/public/loader.test.ts b/src/plugins/expressions/public/loader.test.ts index ff960f4a3a80..f22963cedf61 100644 --- a/src/plugins/expressions/public/loader.test.ts +++ b/src/plugins/expressions/public/loader.test.ts @@ -16,12 +16,14 @@ import { IInterpreterRenderHandlers, RenderMode, AnyExpressionFunctionDefinition, + ExpressionsService, + ExecutionContract, } from '../common'; // eslint-disable-next-line const { __getLastExecution, __getLastRenderMode } = require('./services'); -const element: HTMLElement = null as any; +const element = null as unknown as HTMLElement; let testScheduler: TestScheduler; @@ -36,8 +38,9 @@ jest.mock('./services', () => { }, }; - // eslint-disable-next-line - const service = new (require('../common/service/expressions_services').ExpressionsService as any)(); + const service: ExpressionsService = + // eslint-disable-next-line @typescript-eslint/no-var-requires + new (require('../common/service/expressions_services').ExpressionsService)(); const testFn: AnyExpressionFunctionDefinition = { fn: () => ({ type: 'render', as: 'test' }), @@ -54,9 +57,9 @@ jest.mock('./services', () => { service.start(); + let execution: ExecutionContract; const moduleMock = { - __execution: undefined, - __getLastExecution: () => moduleMock.__execution, + __getLastExecution: () => execution, __getLastRenderMode: () => renderMode, getRenderersRegistry: () => ({ get: (id: string) => renderers[id], @@ -72,13 +75,14 @@ jest.mock('./services', () => { }; const execute = service.execute; - service.execute = (...args: any) => { - const execution = execute(...args); + + jest.spyOn(service, 'execute').mockImplementation((...args) => { + execution = execute(...args); jest.spyOn(execution, 'getData'); jest.spyOn(execution, 'cancel'); - moduleMock.__execution = execution; + return execution; - }; + }); return moduleMock; }); diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index 3ab7473d8d73..b0a54e3dec35 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -9,7 +9,7 @@ import { BehaviorSubject, Observable, Subject, Subscription, asyncScheduler, identity } from 'rxjs'; import { filter, map, delay, throttleTime } from 'rxjs/operators'; import { defaults } from 'lodash'; -import { UnwrapObservable } from '@kbn/utility-types'; +import { SerializableRecord, UnwrapObservable } from '@kbn/utility-types'; import { Adapters } from '../../inspector/public'; import { IExpressionLoaderParams } from './types'; import { ExpressionAstExpression } from '../common'; @@ -18,7 +18,7 @@ import { ExecutionContract } from '../common/execution/execution_contract'; import { ExpressionRenderHandler } from './render'; import { getExpressionsService } from './services'; -type Data = any; +type Data = unknown; export class ExpressionLoader { data$: ReturnType; @@ -156,7 +156,7 @@ export class ExpressionLoader { }; private render(data: Data): void { - this.renderHandler.render(data, this.params.uiState); + this.renderHandler.render(data as SerializableRecord, this.params.uiState); } private setParams(params?: IExpressionLoaderParams) { @@ -169,7 +169,7 @@ export class ExpressionLoader { {}, params.searchContext, this.params.searchContext || {} - ) as any; + ); } if (params.uiState && this.params) { this.params.uiState = params.uiState; diff --git a/src/plugins/expressions/public/react_expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer.test.tsx index d31a4c947b09..f1932ce7dd6b 100644 --- a/src/plugins/expressions/public/react_expression_renderer.test.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.test.tsx @@ -14,6 +14,7 @@ import { ReactExpressionRenderer } from './react_expression_renderer'; import { ExpressionLoader } from './loader'; import { mount } from 'enzyme'; import { EuiProgress } from '@elastic/eui'; +import { IInterpreterRenderHandlers } from '../common'; import { RenderErrorHandlerFnType } from './types'; import { ExpressionRendererEvent } from './render'; @@ -234,7 +235,7 @@ describe('ExpressionRenderer', () => { done: () => { renderSubject.next(1); }, - } as any); + } as IInterpreterRenderHandlers); }); instance.update(); diff --git a/src/plugins/expressions/public/render.test.ts b/src/plugins/expressions/public/render.test.ts index 61dc0a25439b..8d4298785572 100644 --- a/src/plugins/expressions/public/render.test.ts +++ b/src/plugins/expressions/public/render.test.ts @@ -8,6 +8,7 @@ import { ExpressionRenderHandler, render } from './render'; import { Observable } from 'rxjs'; +import { SerializableRecord } from '@kbn/utility-types'; import { ExpressionRenderError } from './types'; import { getRenderersRegistry } from './services'; import { first, take, toArray } from 'rxjs/operators'; @@ -79,11 +80,11 @@ describe('ExpressionRenderHandler', () => { it('in case of error render$ should emit when error renderer is finished', async () => { const expressionRenderHandler = new ExpressionRenderHandler(element); - expressionRenderHandler.render(false); + expressionRenderHandler.render(false as unknown as SerializableRecord); const promise1 = expressionRenderHandler.render$.pipe(first()).toPromise(); await expect(promise1).resolves.toEqual(1); - expressionRenderHandler.render(false); + expressionRenderHandler.render(false as unknown as SerializableRecord); const promise2 = expressionRenderHandler.render$.pipe(first()).toPromise(); await expect(promise2).resolves.toEqual(2); }); @@ -92,7 +93,7 @@ describe('ExpressionRenderHandler', () => { const expressionRenderHandler = new ExpressionRenderHandler(element, { onRenderError: mockMockErrorRenderFunction, }); - await expressionRenderHandler.render(false); + await expressionRenderHandler.render(false as unknown as SerializableRecord); expect(getHandledError()!.message).toEqual( `invalid data provided to the expression renderer` ); @@ -122,7 +123,8 @@ describe('ExpressionRenderHandler', () => { get: () => ({ render: (domNode: HTMLElement, config: unknown, handlers: IInterpreterRenderHandlers) => { handlers.hasCompatibleActions!({ - foo: 'bar', + name: 'something', + data: 'bar', }); }, }), @@ -136,7 +138,8 @@ describe('ExpressionRenderHandler', () => { await expressionRenderHandler.render({ type: 'render', as: 'something' }); expect(hasCompatibleActions).toHaveBeenCalledTimes(1); expect(hasCompatibleActions.mock.calls[0][0]).toEqual({ - foo: 'bar', + name: 'something', + data: 'bar', }); }); @@ -156,7 +159,7 @@ describe('ExpressionRenderHandler', () => { it('default renderer should use notification service', async () => { const expressionRenderHandler = new ExpressionRenderHandler(element); const promise1 = expressionRenderHandler.render$.pipe(first()).toPromise(); - expressionRenderHandler.render(false); + expressionRenderHandler.render(false as unknown as SerializableRecord); await expect(promise1).resolves.toEqual(1); expect(mockNotificationService.toasts.addError).toBeCalledWith( expect.objectContaining({ @@ -175,7 +178,7 @@ describe('ExpressionRenderHandler', () => { const expressionRenderHandler1 = new ExpressionRenderHandler(element, { onRenderError: mockMockErrorRenderFunction, }); - expressionRenderHandler1.render(false); + expressionRenderHandler1.render(false as unknown as SerializableRecord); const renderPromiseAfterRender = expressionRenderHandler1.render$.pipe(first()).toPromise(); await expect(renderPromiseAfterRender).resolves.toEqual(1); expect(getHandledError()!.message).toEqual( @@ -188,7 +191,7 @@ describe('ExpressionRenderHandler', () => { onRenderError: mockMockErrorRenderFunction, }); const renderPromiseBeforeRender = expressionRenderHandler2.render$.pipe(first()).toPromise(); - expressionRenderHandler2.render(false); + expressionRenderHandler2.render(false as unknown as SerializableRecord); await expect(renderPromiseBeforeRender).resolves.toEqual(1); expect(getHandledError()!.message).toEqual( 'invalid data provided to the expression renderer' @@ -199,9 +202,9 @@ describe('ExpressionRenderHandler', () => { // that observables will emit previous result if subscription happens after render it('should emit previous render and error results', async () => { const expressionRenderHandler = new ExpressionRenderHandler(element); - expressionRenderHandler.render(false); + expressionRenderHandler.render(false as unknown as SerializableRecord); const renderPromise = expressionRenderHandler.render$.pipe(take(2), toArray()).toPromise(); - expressionRenderHandler.render(false); + expressionRenderHandler.render(false as unknown as SerializableRecord); await expect(renderPromise).resolves.toEqual([1, 2]); }); }); diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index e9a65d1e8f12..8635a4033bde 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -9,13 +9,20 @@ import * as Rx from 'rxjs'; import { Observable } from 'rxjs'; import { filter } from 'rxjs/operators'; +import { isNumber } from 'lodash'; +import { SerializableRecord } from '@kbn/utility-types'; import { ExpressionRenderError, RenderErrorHandlerFnType, IExpressionLoaderParams } from './types'; import { renderErrorHandler as defaultRenderErrorHandler } from './render_error_handler'; -import { IInterpreterRenderHandlers, ExpressionAstExpression, RenderMode } from '../common'; +import { + IInterpreterRenderHandlers, + IInterpreterRenderEvent, + IInterpreterRenderUpdateParams, + RenderMode, +} from '../common'; import { getRenderersRegistry } from './services'; -export type IExpressionRendererExtraHandlers = Record; +export type IExpressionRendererExtraHandlers = Record; export interface ExpressionRenderHandlerParams { onRenderError?: RenderErrorHandlerFnType; @@ -25,15 +32,10 @@ export interface ExpressionRenderHandlerParams { hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise; } -export interface ExpressionRendererEvent { - name: string; - data: any; -} +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ExpressionRendererEvent = IInterpreterRenderEvent; -interface UpdateValue { - newExpression?: string | ExpressionAstExpression; - newParams: IExpressionLoaderParams; -} +type UpdateValue = IInterpreterRenderUpdateParams; export class ExpressionRenderHandler { render$: Observable; @@ -41,7 +43,7 @@ export class ExpressionRenderHandler { events$: Observable; private element: HTMLElement; - private destroyFn?: any; + private destroyFn?: Function; private renderCount: number = 0; private renderSubject: Rx.BehaviorSubject; private eventsSubject: Rx.Subject; @@ -66,16 +68,14 @@ export class ExpressionRenderHandler { this.onRenderError = onRenderError || defaultRenderErrorHandler; - this.renderSubject = new Rx.BehaviorSubject(null as any | null); - this.render$ = this.renderSubject - .asObservable() - .pipe(filter((_) => _ !== null)) as Observable; + this.renderSubject = new Rx.BehaviorSubject(null); + this.render$ = this.renderSubject.asObservable().pipe(filter(isNumber)); this.updateSubject = new Rx.Subject(); this.update$ = this.updateSubject.asObservable(); this.handlers = { - onDestroy: (fn: any) => { + onDestroy: (fn: Function) => { this.destroyFn = fn; }, done: () => { @@ -104,14 +104,14 @@ export class ExpressionRenderHandler { }; } - render = async (value: any, uiState?: any) => { + render = async (value: SerializableRecord, uiState?: unknown) => { if (!value || typeof value !== 'object') { return this.handleRenderError(new Error('invalid data provided to the expression renderer')); } if (value.type !== 'render' || !value.as) { if (value.type === 'error') { - return this.handleRenderError(value.error); + return this.handleRenderError(value.error as unknown as ExpressionRenderError); } else { return this.handleRenderError( new Error('invalid data provided to the expression renderer') @@ -119,20 +119,20 @@ export class ExpressionRenderHandler { } } - if (!getRenderersRegistry().get(value.as)) { + if (!getRenderersRegistry().get(value.as as string)) { return this.handleRenderError(new Error(`invalid renderer id '${value.as}'`)); } try { // Rendering is asynchronous, completed by handlers.done() await getRenderersRegistry() - .get(value.as)! + .get(value.as as string)! .render(this.element, value.value, { ...this.handlers, uiState, - } as any); + }); } catch (e) { - return this.handleRenderError(e); + return this.handleRenderError(e as ExpressionRenderError); } }; @@ -156,10 +156,10 @@ export class ExpressionRenderHandler { export function render( element: HTMLElement, - data: any, + data: unknown, options?: ExpressionRenderHandlerParams ): ExpressionRenderHandler { const handler = new ExpressionRenderHandler(element, options); - handler.render(data); + handler.render(data as SerializableRecord); return handler; } diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 172f322f8892..ea47403332c7 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -36,7 +36,7 @@ export interface ExpressionInterpreter { export interface IExpressionLoaderParams { searchContext?: SerializableRecord; context?: ExpressionValue; - variables?: Record; + variables?: Record; // Enables debug tracking on each expression in the AST debug?: boolean; disableCaching?: boolean; diff --git a/src/plugins/home/server/services/new_instance_status.ts b/src/plugins/home/server/services/new_instance_status.ts index b72ada27ecba..e19380e92882 100644 --- a/src/plugins/home/server/services/new_instance_status.ts +++ b/src/plugins/home/server/services/new_instance_status.ts @@ -7,7 +7,6 @@ */ import type { IScopedClusterClient, SavedObjectsClientContract } from '../../../../core/server'; -import type { IndexPatternSavedObjectAttrs } from '../../../data/common/data_views/data_views'; const LOGS_INDEX_PATTERN = 'logs-*'; const METRICS_INDEX_PATTERN = 'metrics-*'; @@ -23,7 +22,7 @@ interface Deps { } export const isNewInstance = async ({ esClient, soClient }: Deps): Promise => { - const indexPatterns = await soClient.find({ + const indexPatterns = await soClient.find<{ title: string }>({ type: 'index-pattern', fields: ['title'], search: `*`, diff --git a/src/plugins/kibana_usage_collection/server/collectors/config_usage/README.md b/src/plugins/kibana_usage_collection/server/collectors/config_usage/README.md index b476244e5082..954b12dba00f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/config_usage/README.md +++ b/src/plugins/kibana_usage_collection/server/collectors/config_usage/README.md @@ -55,7 +55,6 @@ when setting an exact config or its parent path to `false`. "server.port": 5603, "server.basePath": "[redacted]", "server.rewriteBasePath": true, - "logging.json": false, "usageCollection.uiCounters.debug": true } } diff --git a/src/plugins/usage_collection/README.mdx b/src/plugins/usage_collection/README.mdx index a6f6f6c8e597..a58f197818bf 100644 --- a/src/plugins/usage_collection/README.mdx +++ b/src/plugins/usage_collection/README.mdx @@ -1,6 +1,6 @@ --- id: kibUsageCollectionPlugin -slug: /kibana-dev-docs/services/usage-collection-plugin +slug: /kibana-dev-docs/key-concepts/usage-collection-plugin title: Usage collection service summary: The Usage Collection Service defines a set of APIs for other plugins to report the usage of their features. date: 2021-02-24 diff --git a/src/plugins/vis_type_markdown/public/markdown_fn.test.ts b/src/plugins/vis_type_markdown/public/markdown_fn.test.ts index bef0b32e392f..148a15fb9fc8 100644 --- a/src/plugins/vis_type_markdown/public/markdown_fn.test.ts +++ b/src/plugins/vis_type_markdown/public/markdown_fn.test.ts @@ -8,6 +8,7 @@ import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; import { createMarkdownVisFn } from './markdown_fn'; +import { Arguments } from './types'; describe('interpreter/functions#markdown', () => { const fn = functionWrapper(createMarkdownVisFn()); @@ -15,7 +16,7 @@ describe('interpreter/functions#markdown', () => { font: { spec: { fontSize: 12 } }, openLinksInNewTab: true, markdown: '## hello _markdown_', - }; + } as unknown as Arguments; it('returns an object with the correct structure', async () => { const actual = await fn(null, args, undefined); diff --git a/src/plugins/vis_types/metric/public/metric_vis_fn.test.ts b/src/plugins/vis_types/metric/public/metric_vis_fn.test.ts index 3844c0f21ed0..28124a653629 100644 --- a/src/plugins/vis_types/metric/public/metric_vis_fn.test.ts +++ b/src/plugins/vis_types/metric/public/metric_vis_fn.test.ts @@ -10,13 +10,15 @@ import { createMetricVisFn } from './metric_vis_fn'; import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils'; import { Datatable } from '../../../expressions/common/expression_types/specs'; +type Arguments = Parameters['fn']>[1]; + describe('interpreter/functions#metric', () => { const fn = functionWrapper(createMetricVisFn()); const context = { type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], - }; + } as unknown as Datatable; const args = { percentageMode: false, useRanges: false, @@ -50,7 +52,7 @@ describe('interpreter/functions#metric', () => { aggType: 'count', }, ], - }; + } as unknown as Arguments; it('returns an object with the correct structure', () => { const actual = fn(context, args, undefined); diff --git a/src/plugins/vis_types/pie/public/pie_fn.test.ts b/src/plugins/vis_types/pie/public/pie_fn.test.ts index d0e0af9807f3..9ba21cdc847e 100644 --- a/src/plugins/vis_types/pie/public/pie_fn.test.ts +++ b/src/plugins/vis_types/pie/public/pie_fn.test.ts @@ -8,6 +8,7 @@ import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils'; import { createPieVisFn } from './pie_fn'; +import { PieVisConfig } from './types'; import { Datatable } from '../../../expressions/common/expression_types/specs'; describe('interpreter/functions#pie', () => { @@ -16,7 +17,7 @@ describe('interpreter/functions#pie', () => { type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], - }; + } as unknown as Datatable; const visConfig = { addTooltip: true, addLegend: true, @@ -43,7 +44,7 @@ describe('interpreter/functions#pie', () => { params: {}, aggType: 'count', }, - }; + } as unknown as PieVisConfig; beforeEach(() => { jest.clearAllMocks(); diff --git a/src/plugins/vis_types/table/public/table_vis_fn.test.ts b/src/plugins/vis_types/table/public/table_vis_fn.test.ts index 8b08bca16047..d0d9a414bbcc 100644 --- a/src/plugins/vis_types/table/public/table_vis_fn.test.ts +++ b/src/plugins/vis_types/table/public/table_vis_fn.test.ts @@ -8,6 +8,7 @@ import { createTableVisFn } from './table_vis_fn'; import { tableVisResponseHandler } from './utils'; +import { TableVisConfig } from './types'; import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils'; import { Datatable } from '../../../expressions/common/expression_types/specs'; @@ -24,7 +25,7 @@ describe('interpreter/functions#table', () => { type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], - }; + } as unknown as Datatable; const visConfig = { title: 'My Chart title', perPage: 10, @@ -52,7 +53,7 @@ describe('interpreter/functions#table', () => { }, ], buckets: [], - }; + } as unknown as TableVisConfig; beforeEach(() => { jest.clearAllMocks(); diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx index 1029ac67cc43..d28e2c5e0fb9 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx @@ -33,22 +33,28 @@ export interface IndexPatternSelectProps { | null; } -const defaultIndexPatternHelpText = i18n.translate( - 'visTypeTimeseries.indexPatternSelect.defaultIndexPatternText', - { - defaultMessage: 'Default index pattern is used.', - } +const queryAllIndicesHelpText = ( + *, + }} + /> ); -const queryAllIndexesHelpText = i18n.translate( - 'visTypeTimeseries.indexPatternSelect.queryAllIndexesText', - { - defaultMessage: 'To query all indexes use *', - } +const getIndexPatternHelpText = (useKibanaIndices: boolean) => ( + ); const indexPatternLabel = i18n.translate('visTypeTimeseries.indexPatternSelect.label', { - defaultMessage: 'Index pattern', + defaultMessage: 'Data view', }); export const IndexPatternSelect = ({ @@ -103,17 +109,14 @@ export const IndexPatternSelect = ({ diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/switch_mode_popover.tsx b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/switch_mode_popover.tsx index 43ef091da251..5e66b50eac46 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/switch_mode_popover.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/switch_mode_popover.tsx @@ -79,7 +79,7 @@ export const SwitchModePopover = ({ onModeChange, useKibanaIndices }: PopoverPro aria-label={i18n.translate( 'visTypeTimeseries.indexPatternSelect.switchModePopover.areaLabel', { - defaultMessage: 'Configure index pattern selection mode', + defaultMessage: 'Configure data view selection mode', } )} onClick={onButtonClick} @@ -97,14 +97,13 @@ export const SwitchModePopover = ({ onModeChange, useKibanaIndices }: PopoverPro > {i18n.translate('visTypeTimeseries.indexPatternSelect.switchModePopover.title', { - defaultMessage: 'Index pattern selection mode', + defaultMessage: 'Data view mode', })} { { } iconType="cheer" @@ -42,13 +42,13 @@ export const UseIndexPatternModeCallout = () => { >

@@ -59,7 +59,7 @@ export const UseIndexPatternModeCallout = () => { diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js index 4257c35a6d4c..208f9af9bb25 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js @@ -538,8 +538,8 @@ export const TimeseriesConfig = injectI18n(function (props) { { + test('should format the TSVB visState correctly', () => { + const visState = { + title: 'test', + type: 'metrics', + aggs: [], + params: { + time_range_mode: 'entire_time_range', + id: '0ecc58b1-30ba-43b9-aa3f-9ac32b482497', + type: 'timeseries', + series: [ + { + id: '1', + color: '#68BC00', + split_mode: 'terms', + palette: { + type: 'palette', + name: 'default', + }, + metrics: [ + { + id: '10', + type: 'count', + }, + ], + separate_axis: 0, + axis_position: 'right', + formatter: 'default', + chart_type: 'line', + line_width: 1, + point_size: 1, + fill: 0.5, + stacked: 'none', + terms_field: 'Cancelled', + }, + ], + time_field: '', + use_kibana_indexes: true, + interval: '', + axis_position: 'left', + axis_formatter: 'number', + axis_scale: 'normal', + show_legend: 1, + truncate_legend: 1, + max_lines_legend: 1, + show_grid: 1, + tooltip_mode: 'show_all', + drop_last_bucket: 0, + isModelInvalid: false, + index_pattern: { + id: '665cd2c0-21d6-11ec-b42f-f7077c64d21b', + }, + }, + }; + const newVisState = updateOldState(visState); + expect(newVisState).toEqual({ + aggs: [], + params: { + axis_formatter: 'number', + axis_position: 'left', + axis_scale: 'normal', + drop_last_bucket: 0, + id: '0ecc58b1-30ba-43b9-aa3f-9ac32b482497', + index_pattern: { + id: '665cd2c0-21d6-11ec-b42f-f7077c64d21b', + }, + interval: '', + isModelInvalid: false, + max_lines_legend: 1, + series: [ + { + axis_position: 'right', + chart_type: 'line', + color: '#68BC00', + fill: 0.5, + formatter: 'default', + id: 'x1', + line_width: 1, + metrics: [ + { + id: 'x10', + type: 'count', + }, + ], + palette: { + name: 'default', + type: 'palette', + }, + point_size: 1, + separate_axis: 0, + split_mode: 'terms', + stacked: 'none', + terms_field: 'Cancelled', + }, + ], + show_grid: 1, + show_legend: 1, + time_field: '', + time_range_mode: 'entire_time_range', + tooltip_mode: 'show_all', + truncate_legend: 1, + type: 'timeseries', + use_kibana_indexes: true, + }, + title: 'test', + type: 'metrics', + }); + }); +}); diff --git a/src/plugins/vis_types/vislib/public/pie_fn.test.ts b/src/plugins/vis_types/vislib/public/pie_fn.test.ts index 0df7bf1365be..9c317f9e72dc 100644 --- a/src/plugins/vis_types/vislib/public/pie_fn.test.ts +++ b/src/plugins/vis_types/vislib/public/pie_fn.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { Datatable } from 'src/plugins/expressions'; import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils'; import { createPieVisFn } from './pie_fn'; // @ts-ignore @@ -34,7 +35,7 @@ describe('interpreter/functions#pie', () => { type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], - }; + } as unknown as Datatable; const visConfig = { type: 'pie', addTooltip: true, diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.js b/src/plugins/visualizations/public/legacy/vis_update_state.js index d0ebe00b1a6f..db6a9f2beb77 100644 --- a/src/plugins/visualizations/public/legacy/vis_update_state.js +++ b/src/plugins/visualizations/public/legacy/vis_update_state.js @@ -136,6 +136,30 @@ function convertSeriesParams(visState) { ]; } +/** + * This function is responsible for updating old TSVB visStates. + * Specifically, it identifies if the series and metrics ids are numbers + * and convert them to string with an x prefix. Number ids are never been generated + * from the editor, only programmatically. See https://github.com/elastic/kibana/issues/113601. + */ +function convertNumIdsToStringsForTSVB(visState) { + if (visState.params.series) { + visState.params.series.forEach((s) => { + const seriesId = s.id; + const metrics = s.metrics; + if (!isNaN(seriesId)) { + s.id = `x${seriesId}`; + } + metrics?.forEach((m) => { + const metricId = m.id; + if (!isNaN(metricId)) { + m.id = `x${metricId}`; + } + }); + }); + } +} + /** * This function is responsible for updating old visStates - the actual saved object * object - into the format, that will be required by the current Kibana version. @@ -155,6 +179,10 @@ export const updateOldState = (visState) => { convertSeriesParams(newState); } + if (visState.params && visState.type === 'metrics') { + convertNumIdsToStringsForTSVB(newState); + } + if (visState.type === 'gauge' && visState.fontSize) { delete newState.fontSize; set(newState, 'gauge.style.fontSize', visState.fontSize); diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.test.js b/src/plugins/visualizations/public/legacy/vis_update_state.test.js index 3b0d732df2d1..a7c2df506d31 100644 --- a/src/plugins/visualizations/public/legacy/vis_update_state.test.js +++ b/src/plugins/visualizations/public/legacy/vis_update_state.test.js @@ -93,4 +93,87 @@ describe('updateOldState', () => { expect(state.params.showMeticsAtAllLevels).toBe(undefined); }); }); + + describe('TSVB ids conversion', () => { + it('should update the seriesId from number to string with x prefix', () => { + const oldState = { + type: 'metrics', + params: { + series: [ + { + id: '10', + }, + { + id: 'ABC', + }, + { + id: 1, + }, + ], + }, + }; + const state = updateOldState(oldState); + expect(state.params.series).toEqual([ + { + id: 'x10', + }, + { + id: 'ABC', + }, + { + id: 'x1', + }, + ]); + }); + it('should update the metrics ids from number to string with x prefix', () => { + const oldState = { + type: 'metrics', + params: { + series: [ + { + id: '10', + metrics: [ + { + id: '1000', + }, + { + id: '74a66e70-ac44-11eb-9865-6b616e971cf8', + }, + ], + }, + { + id: 'ABC', + metrics: [ + { + id: null, + }, + ], + }, + ], + }, + }; + const state = updateOldState(oldState); + expect(state.params.series).toEqual([ + { + id: 'x10', + metrics: [ + { + id: 'x1000', + }, + { + id: '74a66e70-ac44-11eb-9865-6b616e971cf8', + }, + ], + }, + { + id: 'ABC', + metrics: [ + { + id: 'xnull', + }, + ], + }, + ]); + }); + }); }); diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts index 7a0bb4584e83..31713d8ad7d5 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts @@ -8,7 +8,7 @@ import { SavedObjectReference } from '../../../../../core/types'; import { VisParams } from '../../../common'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../../data/public'; +import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../../data/common'; const isControlsVis = (visType: string) => visType === 'input_control_vis'; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts index 98970a0127c7..a3917699fcab 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts @@ -8,7 +8,7 @@ import { SavedObjectReference } from '../../../../../core/types'; import { VisParams } from '../../../common'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../../data/public'; +import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../../data/common'; /** @internal **/ const REF_NAME_POSTFIX = '_ref_name'; diff --git a/test/api_integration/apis/index_patterns/deprecations/scripted_fields.ts b/test/api_integration/apis/index_patterns/deprecations/scripted_fields.ts index 168c2b005d80..c32b9b424120 100644 --- a/test/api_integration/apis/index_patterns/deprecations/scripted_fields.ts +++ b/test/api_integration/apis/index_patterns/deprecations/scripted_fields.ts @@ -30,7 +30,9 @@ export default function ({ getService }: FtrProviderContext) { const { body } = await supertest.get('/api/deprecations/'); const { deprecations } = body as DeprecationsGetResponse; - const dataPluginDeprecations = deprecations.filter(({ domainId }) => domainId === 'data'); + const dataPluginDeprecations = deprecations.filter( + ({ domainId }) => domainId === 'dataViews' + ); expect(dataPluginDeprecations.length).to.be(0); }); @@ -59,7 +61,9 @@ export default function ({ getService }: FtrProviderContext) { const { body } = await supertest.get('/api/deprecations/'); const { deprecations } = body as DeprecationsGetResponse; - const dataPluginDeprecations = deprecations.filter(({ domainId }) => domainId === 'data'); + const dataPluginDeprecations = deprecations.filter( + ({ domainId }) => domainId === 'dataViews' + ); expect(dataPluginDeprecations.length).to.be(1); expect(dataPluginDeprecations[0].message).to.contain(title); diff --git a/test/api_integration/apis/index_patterns/es_errors/errors.js b/test/api_integration/apis/index_patterns/es_errors/errors.js index ac656e487323..d5ca92c37161 100644 --- a/test/api_integration/apis/index_patterns/es_errors/errors.js +++ b/test/api_integration/apis/index_patterns/es_errors/errors.js @@ -15,7 +15,7 @@ import { createNoMatchingIndicesError, isNoMatchingIndicesError, convertEsError, -} from '../../../../../src/plugins/data/server/data_views/fetcher/lib/errors'; +} from '../../../../../src/plugins/data_views/server/fetcher/lib/errors'; import { getIndexNotFoundError, getDocNotFoundError } from './lib'; diff --git a/test/common/config.js b/test/common/config.js index eb110fad55ea..b9ab24450ac8 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -28,7 +28,6 @@ export default function () { buildArgs: [], sourceArgs: ['--no-base-path', '--env.name=development'], serverArgs: [ - '--logging.json=false', `--server.port=${kbnTestConfig.getPort()}`, '--status.allowAnonymous=true', // We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should diff --git a/test/common/services/saved_object_info/index.ts b/test/common/services/saved_object_info/index.ts index 799c9964fde7..41367694373f 100644 --- a/test/common/services/saved_object_info/index.ts +++ b/test/common/services/saved_object_info/index.ts @@ -33,7 +33,7 @@ Show information pertaining to the saved objects in the .kibana index Examples: -See 'README.md' +See 'saved_objects_info_svc.md' `, flags: expectedFlags(), diff --git a/test/common/services/saved_object_info/saved_object_info.ts b/test/common/services/saved_object_info/saved_object_info.ts index a75dfd8f3b5a..61472ea98d87 100644 --- a/test/common/services/saved_object_info/saved_object_info.ts +++ b/test/common/services/saved_object_info/saved_object_info.ts @@ -13,7 +13,6 @@ import { flow, pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/lib/TaskEither'; import * as T from 'fp-ts/lib/Task'; import { ToolingLog } from '@kbn/dev-utils'; -import { run as jq } from 'node-jq'; import { FtrService } from '../../ftr_provider_context'; import { print } from './utils'; @@ -61,22 +60,8 @@ export const types = export class SavedObjectInfoService extends FtrService { private readonly config = this.ctx.getService('config'); - private readonly typesF = async () => - await types(url.format(this.config.get('servers.elasticsearch')))(); - public async logSoTypes(log: ToolingLog, msg: string | null = null) { // @ts-ignore - pipe(await this.typesF(), print(log)(msg)); - } - - /** - * See test/common/services/saved_object_info/README.md for "jq filtering" ideas. - */ - public async filterSoTypes(log: ToolingLog, jqFilter: string, title: string | null = null) { - pipe(await this.typesF(), filterAndLog); - - async function filterAndLog(payload: any) { - log.info(`${title ? title + '\n' : ''}${await jq(jqFilter, payload, { input: 'json' })}`); - } + pipe(await types(url.format(this.config.get('servers.elasticsearch'))), print(log)(msg)); } } diff --git a/test/common/services/saved_object_info/saved_objects_info_svc.md b/test/common/services/saved_object_info/saved_objects_info_svc.md new file mode 100644 index 000000000000..2d623129e290 --- /dev/null +++ b/test/common/services/saved_object_info/saved_objects_info_svc.md @@ -0,0 +1,35 @@ +# Saved Objects Info Svc w/ CLI + +## Used via the cli + +Run the cli +> the **--esUrl** arg is required; tells the svc which elastic search endpoint to use + +```shell + λ node scripts/saved_objs_info.js --esUrl http://elastic:changeme@localhost:9220 --soTypes +``` + +Result + +```shell + ### types: + + [ + { + doc_count: 5, + key: 'canvas-workpad-template' + }, + { + doc_count: 1, + key: 'apm-telemetry' + }, + { + doc_count: 1, + key: 'config' + }, + { + doc_count: 1, + key: 'space' + } + ] +``` diff --git a/test/common/services/saved_object_info/README.md b/test/common/services/saved_object_info/use_with_jq.md similarity index 53% rename from test/common/services/saved_object_info/README.md rename to test/common/services/saved_object_info/use_with_jq.md index c5e36f2596dd..5f081e48e263 100644 --- a/test/common/services/saved_object_info/README.md +++ b/test/common/services/saved_object_info/use_with_jq.md @@ -1,70 +1,6 @@ -# Tips for using the SO INFO SVC +# Tips for using the SO INFO SVC CLI with JQ -## From an FTR test -``` - ... - const soInfo = getService('savedObjectInfo'); - const log = getService('log'); - - describe('some test suite', function () { - ... - - after(async () => { - // "Normal" logging, without JQ - await soInfo.logSoTypes(log); - // Without a title, using JQ - await soInfo.filterSoTypes(log, '.[] | .key'); - // With a title, using JQ - await soInfo.filterSoTypes( - log, - 'reduce .[].doc_count as $item (0; . + $item)', - 'TOTAL count of ALL Saved Object types' - ); - // With a title, using JQ - await soInfo.filterSoTypes( - log, - '.[] | select(.key =="canvas-workpad-template") | .doc_count', - 'TOTAL count of canvas-workpad-template' - ); - }); -``` - -## From the CLI - -Run the cli -> the **--esUrl** arg is required; tells the svc which elastic search endpoint to use - -```shell - λ node scripts/saved_objs_info.js --esUrl http://elastic:changeme@localhost:9220 --soTypes -``` - -Result - -```shell - ### types: - - [ - { - doc_count: 5, - key: 'canvas-workpad-template' - }, - { - doc_count: 1, - key: 'apm-telemetry' - }, - { - doc_count: 1, - key: 'config' - }, - { - doc_count: 1, - key: 'space' - } - ] -``` - - -### Myriad ways to use JQ to discern discrete info from the svc +## Myriad ways to use jq to discern discrete info from the svc Below, I will leave out the so types call, which is: `node scripts/saved_objs_info.js --esUrl http://elastic:changeme@localhost:9220 --soTypes --json` diff --git a/test/common/services/saved_object_info/utils.ts b/test/common/services/saved_object_info/utils.ts index 64b39e5ebeed..f007710f2b14 100644 --- a/test/common/services/saved_object_info/utils.ts +++ b/test/common/services/saved_object_info/utils.ts @@ -43,7 +43,7 @@ export const expectedFlags = () => ({ string: ['esUrl'], boolean: ['soTypes', 'json'], help: ` ---esUrl Required, tells the app which url to point to +--esUrl Required, tells the svc which url to point to --soTypes Not Required, tells the svc to show the types within the .kibana index --json Not Required, tells the svc to show the types, with only json output. Useful for piping into jq `, diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.ts b/test/functional/apps/dashboard/create_and_add_embeddables.ts index 62ce68e026f7..6af295d2cf85 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.ts +++ b/test/functional/apps/dashboard/create_and_add_embeddables.ts @@ -68,6 +68,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); }); + it('adds a new timelion visualization', async () => { + // adding this case, as the timelion agg-based viz doesn't need the `clickNewSearch()` step + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); + await PageObjects.visualize.clickTimelion(); + await PageObjects.visualize.saveVisualizationExpectSuccess( + 'timelion visualization from add new link', + { redirectToOrigin: true } + ); + + await retry.try(async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(originalPanelCount + 1); + }); + await PageObjects.dashboard.waitForRenderComplete(); + }); + it('adds a markdown visualization via the quick button', async () => { const originalPanelCount = await PageObjects.dashboard.getPanelCount(); await dashboardAddPanel.clickMarkdownQuickButton(); diff --git a/test/functional/apps/dashboard_elements/input_control_vis/input_control_range.ts b/test/functional/apps/dashboard_elements/input_control_vis/input_control_range.ts index 566e6f033d2f..29c914d76a8c 100644 --- a/test/functional/apps/dashboard_elements/input_control_vis/input_control_range.ts +++ b/test/functional/apps/dashboard_elements/input_control_vis/input_control_range.ts @@ -18,7 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); - describe('input control range', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/113744 + describe.skip('input control range', () => { before(async () => { await PageObjects.visualize.initTests(); await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index 4edc4d22f075..0a8f56ee250e 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -233,11 +233,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('time zone switch', () => { it('should show bars in the correct time zone after switching', async function () { - await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' }); + await kibanaServer.uiSettings.update({ 'dateFormat:tz': 'America/Phoenix' }); await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitKibanaChrome(); - await queryBar.clearQuery(); await PageObjects.timePicker.setDefaultAbsoluteRange(); + await queryBar.clearQuery(); log.debug( 'check that the newest doc timestamp is now -7 hours from the UTC time in the first test' @@ -246,36 +246,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(rowData.startsWith('Sep 22, 2015 @ 16:50:13.253')).to.be.ok(); }); }); - describe('usage of discover:searchOnPageLoad', () => { - it('should not fetch data from ES initially when discover:searchOnPageLoad is false', async function () { - await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': false }); - await PageObjects.common.navigateToApp('discover'); - await PageObjects.header.awaitKibanaChrome(); - - expect(await PageObjects.discover.getNrOfFetches()).to.be(0); - }); - - it('should fetch data from ES initially when discover:searchOnPageLoad is true', async function () { - await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': true }); - await PageObjects.common.navigateToApp('discover'); - await PageObjects.header.awaitKibanaChrome(); - await retry.waitFor('number of fetches to be 1', async () => { - const nrOfFetches = await PageObjects.discover.getNrOfFetches(); - return nrOfFetches === 1; - }); - }); - }); describe('invalid time range in URL', function () { it('should get the default timerange', async function () { - const prevTime = await PageObjects.timePicker.getTimeConfig(); await PageObjects.common.navigateToUrl('discover', '#/?_g=(time:(from:now-15m,to:null))', { useActualUrl: true, }); await PageObjects.header.awaitKibanaChrome(); const time = await PageObjects.timePicker.getTimeConfig(); - expect(time.start).to.be(prevTime.start); - expect(time.end).to.be(prevTime.end); + expect(time.start).to.be('~ 15 minutes ago'); + expect(time.end).to.be('now'); }); }); diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts index de12cde84edc..36abcd81d53a 100644 --- a/test/functional/apps/discover/_discover_histogram.ts +++ b/test/functional/apps/discover/_discover_histogram.ts @@ -52,21 +52,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } it('should visualize monthly data with different day intervals', async () => { - const fromTime = 'Nov 01, 2017 @ 00:00:00.000'; + const fromTime = 'Nov 1, 2017 @ 00:00:00.000'; const toTime = 'Mar 21, 2018 @ 00:00:00.000'; await prepareTest(fromTime, toTime, 'Month'); const chartCanvasExist = await elasticChart.canvasExists(); expect(chartCanvasExist).to.be(true); }); it('should visualize weekly data with within DST changes', async () => { - const fromTime = 'Mar 01, 2018 @ 00:00:00.000'; - const toTime = 'May 01, 2018 @ 00:00:00.000'; + const fromTime = 'Mar 1, 2018 @ 00:00:00.000'; + const toTime = 'May 1, 2018 @ 00:00:00.000'; await prepareTest(fromTime, toTime, 'Week'); const chartCanvasExist = await elasticChart.canvasExists(); expect(chartCanvasExist).to.be(true); }); it('should visualize monthly data with different years scaled to 30 days', async () => { - const fromTime = 'Jan 01, 2010 @ 00:00:00.000'; + const fromTime = 'Jan 1, 2010 @ 00:00:00.000'; const toTime = 'Mar 21, 2019 @ 00:00:00.000'; await prepareTest(fromTime, toTime, 'Day'); const chartCanvasExist = await elasticChart.canvasExists(); @@ -75,7 +75,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(chartIntervalIconTip).to.be(true); }); it('should allow hide/show histogram, persisted in url state', async () => { - const fromTime = 'Jan 01, 2010 @ 00:00:00.000'; + const fromTime = 'Jan 1, 2010 @ 00:00:00.000'; const toTime = 'Mar 21, 2019 @ 00:00:00.000'; await prepareTest(fromTime, toTime); let canvasExists = await elasticChart.canvasExists(); @@ -95,7 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(canvasExists).to.be(true); }); it('should allow hiding the histogram, persisted in saved search', async () => { - const fromTime = 'Jan 01, 2010 @ 00:00:00.000'; + const fromTime = 'Jan 1, 2010 @ 00:00:00.000'; const toTime = 'Mar 21, 2019 @ 00:00:00.000'; const savedSearch = 'persisted hidden histogram'; await prepareTest(fromTime, toTime); diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 20f2cab907d9..832d895fcea3 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -17,39 +17,83 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); const browser = getService('browser'); - - const defaultSettings = { - defaultIndex: 'logstash-*', - }; const filterBar = getService('filterBar'); const queryBar = getService('queryBar'); const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const testSubjects = getService('testSubjects'); + const defaultSettings = { + defaultIndex: 'logstash-*', + }; + + const setUpQueriesWithFilters = async () => { + // set up a query with filters and a time filter + log.debug('set up a query with filters to save'); + const fromTime = 'Sep 20, 2015 @ 08:00:00.000'; + const toTime = 'Sep 21, 2015 @ 08:00:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await filterBar.addFilter('extension.raw', 'is one of', 'jpg'); + await queryBar.setQuery('response:200'); + }; describe('saved queries saved objects', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); + await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); + await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); - // and load a set of makelogs data - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace(defaultSettings); log.debug('discover'); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); }); - describe('saved query management component functionality', function () { - before(async function () { - // set up a query with filters and a time filter - log.debug('set up a query with filters to save'); - await queryBar.setQuery('response:200'); - await filterBar.addFilter('extension.raw', 'is one of', 'jpg'); - const fromTime = 'Sep 20, 2015 @ 08:00:00.000'; - const toTime = 'Sep 21, 2015 @ 08:00:00.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + }); + + describe('saved query selection', () => { + before(async () => await setUpQueriesWithFilters()); + + it(`should unselect saved query when navigating to a 'new'`, async function () { + await savedQueryManagementComponent.saveNewQuery( + 'test-unselect-saved-query', + 'mock', + true, + true + ); + + await queryBar.submitQuery(); + + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); + expect(await queryBar.getQueryString()).to.eql('response:200'); + + await PageObjects.discover.clickNewSearchButton(); + + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); + expect(await queryBar.getQueryString()).to.eql(''); + + await PageObjects.discover.selectIndexPattern('date-nested'); + + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); + expect(await queryBar.getQueryString()).to.eql(''); + + await PageObjects.discover.selectIndexPattern('logstash-*'); + + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); + expect(await queryBar.getQueryString()).to.eql(''); + + // reset state + await savedQueryManagementComponent.deleteSavedQuery('test-unselect-saved-query'); }); + }); + + describe('saved query management component functionality', function () { + before(async () => await setUpQueriesWithFilters()); it('should show the saved query management component when there are no saved queries', async () => { await savedQueryManagementComponent.openSavedQueryManagementComponent(); diff --git a/test/functional/apps/discover/_search_on_page_load.ts b/test/functional/apps/discover/_search_on_page_load.ts new file mode 100644 index 000000000000..2a66e03c3cbb --- /dev/null +++ b/test/functional/apps/discover/_search_on_page_load.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const log = getService('log'); + const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const queryBar = getService('queryBar'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const testSubjects = getService('testSubjects'); + + const defaultSettings = { + defaultIndex: 'logstash-*', + }; + + const initSearchOnPageLoad = async (searchOnPageLoad: boolean) => { + await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': searchOnPageLoad }); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.awaitKibanaChrome(); + }; + + const waitForFetches = (fetchesNumber: number) => async () => { + const nrOfFetches = await PageObjects.discover.getNrOfFetches(); + return nrOfFetches === fetchesNumber; + }; + + describe('usage of discover:searchOnPageLoad', () => { + before(async function () { + log.debug('load kibana index with default index pattern'); + + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); + + // and load a set of data + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); + + await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + }); + + describe(`when it's false`, () => { + beforeEach(async () => await initSearchOnPageLoad(false)); + + it('should not fetch data from ES initially', async function () { + expect(await testSubjects.exists('refreshDataButton')).to.be(true); + await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + }); + + it('should not fetch on indexPattern change', async function () { + expect(await testSubjects.exists('refreshDataButton')).to.be(true); + await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + + await PageObjects.discover.selectIndexPattern('date-nested'); + + expect(await testSubjects.exists('refreshDataButton')).to.be(true); + await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + }); + + it('should fetch data from ES after refreshDataButton click', async function () { + expect(await testSubjects.exists('refreshDataButton')).to.be(true); + await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + + await testSubjects.click('refreshDataButton'); + + await retry.waitFor('number of fetches to be 1', waitForFetches(1)); + expect(await testSubjects.exists('refreshDataButton')).to.be(false); + }); + + it('should fetch data from ES after submit query', async function () { + expect(await testSubjects.exists('refreshDataButton')).to.be(true); + await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + + await queryBar.submitQuery(); + + await retry.waitFor('number of fetches to be 1', waitForFetches(1)); + expect(await testSubjects.exists('refreshDataButton')).to.be(false); + }); + + it('should fetch data from ES after choosing commonly used time range', async function () { + await PageObjects.discover.selectIndexPattern('logstash-*'); + expect(await testSubjects.exists('refreshDataButton')).to.be(true); + await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + + await PageObjects.timePicker.setCommonlyUsedTime('This_week'); + + await retry.waitFor('number of fetches to be 1', waitForFetches(1)); + expect(await testSubjects.exists('refreshDataButton')).to.be(false); + }); + }); + + it(`when it's false should fetch data from ES initially`, async function () { + await initSearchOnPageLoad(true); + await retry.waitFor('number of fetches to be 1', waitForFetches(1)); + }); + }); +} diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index 3a18a55fe138..59191b489f4c 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -51,5 +51,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_runtime_fields_editor')); loadTestFile(require.resolve('./_huge_fields')); loadTestFile(require.resolve('./_date_nested')); + loadTestFile(require.resolve('./_search_on_page_load')); }); } diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index 4aa06f4cd9ad..2e965c275d6d 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -367,7 +367,8 @@ export default function ({ getService, getPageObjects }) { }); }); - describe('creating and using Painless date scripted fields', function describeIndexTests() { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/113745 + describe.skip('creating and using Painless date scripted fields', function describeIndexTests() { const scriptedPainlessFieldName2 = 'painDate'; it('should create scripted field', async function () { diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 853a926f4f6e..64fb184f40e4 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -67,10 +67,14 @@ export class CommonPageObject extends FtrService { await this.loginPage.login('test_user', 'changeme'); } - await this.find.byCssSelector( - '[data-test-subj="kibanaChrome"] nav:not(.ng-hide)', - 6 * this.defaultFindTimeout - ); + if (appUrl.includes('/status')) { + await this.testSubjects.find('statusPageRoot'); + } else { + await this.find.byCssSelector( + '[data-test-subj="kibanaChrome"] nav:not(.ng-hide)', + 6 * this.defaultFindTimeout + ); + } await this.browser.get(appUrl, insertTimestamp); currentUrl = await this.browser.getCurrentUrl(); this.log.debug(`Finished login process currentUrl = ${currentUrl}`); @@ -217,8 +221,9 @@ export class CommonPageObject extends FtrService { { basePath = '', shouldLoginIfPrompted = true, - disableWelcomePrompt = true, hash = '', + search = '', + disableWelcomePrompt = true, insertTimestamp = true, } = {} ) { @@ -229,11 +234,13 @@ export class CommonPageObject extends FtrService { appUrl = getUrl.noAuth(this.config.get('servers.kibana'), { pathname: `${basePath}${appConfig.pathname}`, hash: hash || appConfig.hash, + search, }); } else { appUrl = getUrl.noAuth(this.config.get('servers.kibana'), { pathname: `${basePath}/app/${appName}`, hash, + search, }); } diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 00133d720d88..6cc4fda513ea 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -116,23 +116,38 @@ export class TimePickerPageObject extends FtrService { public async setAbsoluteRange(fromTime: string, toTime: string) { this.log.debug(`Setting absolute range to ${fromTime} to ${toTime}`); await this.showStartEndTimes(); + let panel!: WebElementWrapper; // set to time - await this.testSubjects.click('superDatePickerendDatePopoverButton'); - let panel = await this.getTimePickerPanel(); - await this.testSubjects.click('superDatePickerAbsoluteTab'); - await this.testSubjects.click('superDatePickerAbsoluteDateInput'); - await this.inputValue('superDatePickerAbsoluteDateInput', toTime); - await this.browser.pressKeys(this.browser.keys.ESCAPE); // close popover because sometimes browser can't find start input + await this.retry.waitFor(`endDate is set to ${toTime}`, async () => { + await this.testSubjects.click('superDatePickerendDatePopoverButton'); + panel = await this.getTimePickerPanel(); + await this.testSubjects.click('superDatePickerAbsoluteTab'); + await this.testSubjects.click('superDatePickerAbsoluteDateInput'); + await this.inputValue('superDatePickerAbsoluteDateInput', toTime); + await this.browser.pressKeys(this.browser.keys.ESCAPE); // close popover because sometimes browser can't find start input + const actualToTime = await this.testSubjects.getVisibleText( + 'superDatePickerendDatePopoverButton' + ); + this.log.debug(`Validating 'endDate' - expected: '${toTime}, actual: ${actualToTime}'`); + return toTime === actualToTime; + }); // set from time - await this.testSubjects.click('superDatePickerstartDatePopoverButton'); - await this.waitPanelIsGone(panel); - panel = await this.getTimePickerPanel(); - await this.testSubjects.click('superDatePickerAbsoluteTab'); - await this.testSubjects.click('superDatePickerAbsoluteDateInput'); - await this.inputValue('superDatePickerAbsoluteDateInput', fromTime); - await this.browser.pressKeys(this.browser.keys.ESCAPE); + await this.retry.waitFor(`endDate is set to ${fromTime}`, async () => { + await this.testSubjects.click('superDatePickerstartDatePopoverButton'); + await this.waitPanelIsGone(panel); + panel = await this.getTimePickerPanel(); + await this.testSubjects.click('superDatePickerAbsoluteTab'); + await this.testSubjects.click('superDatePickerAbsoluteDateInput'); + await this.inputValue('superDatePickerAbsoluteDateInput', fromTime); + await this.browser.pressKeys(this.browser.keys.ESCAPE); + const actualFromTime = await this.testSubjects.getVisibleText( + 'superDatePickerstartDatePopoverButton' + ); + this.log.debug(`Validating 'startDate' - expected: '${fromTime}, actual: ${actualFromTime}'`); + return fromTime === actualFromTime; + }); await this.retry.waitFor('Timepicker popover to close', async () => { return !(await this.testSubjects.exists('superDatePickerAbsoluteDateInput')); diff --git a/test/server_integration/http/platform/config.status.ts b/test/server_integration/http/platform/config.status.ts index 8cc76c901f47..20ffc917f824 100644 --- a/test/server_integration/http/platform/config.status.ts +++ b/test/server_integration/http/platform/config.status.ts @@ -51,7 +51,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { runOptions: { ...httpConfig.get('kbnTestServer.runOptions'), // Don't wait for Kibana to be completely ready so that we can test the status timeouts - wait: /\[Kibana\]\[http\] http server running/, + wait: /Kibana is now unavailable/, }, }, }; diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 729286b59408..3f7cdecf4aff 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -1696,6 +1696,223 @@ describe('successful migrations', () => { }, }); }); + + test('security solution is migrated to saved object references if it has a "ruleAlertId"', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = getMockData({ + alertTypeId: 'siem.notifications', + params: { + ruleAlertId: '123', + }, + }); + + expect(migration7160(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + legacyId: alert.id, + }, + references: [ + { + name: 'param:alert_0', + id: '123', + type: 'alert', + }, + ], + }); + }); + + test('security solution does not migrate anything if its type is not siem.notifications', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = getMockData({ + alertTypeId: 'other-type', + params: { + ruleAlertId: '123', + }, + }); + + expect(migration7160(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + legacyId: alert.id, + }, + }); + }); + test('security solution does not change anything if "ruleAlertId" is missing', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = getMockData({ + alertTypeId: 'siem.notifications', + params: {}, + }); + + expect(migration7160(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + legacyId: alert.id, + }, + }); + }); + + test('security solution will keep existing references if we do not have a "ruleAlertId" but we do already have references', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.notifications', + params: {}, + }), + references: [ + { + name: 'param:alert_0', + id: '123', + type: 'alert', + }, + ], + }; + + expect(migration7160(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + legacyId: alert.id, + }, + references: [ + { + name: 'param:alert_0', + id: '123', + type: 'alert', + }, + ], + }); + }); + + test('security solution will keep any foreign references if they exist but still migrate other "ruleAlertId" references', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.notifications', + params: { + ruleAlertId: '123', + }, + }), + references: [ + { + name: 'foreign-name', + id: '999', + type: 'foreign-name', + }, + ], + }; + + expect(migration7160(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + legacyId: alert.id, + }, + references: [ + { + name: 'foreign-name', + id: '999', + type: 'foreign-name', + }, + { + name: 'param:alert_0', + id: '123', + type: 'alert', + }, + ], + }); + }); + + test('security solution is idempotent and if re-run on the same migrated data will keep the same items "ruleAlertId" references', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.notifications', + params: { + ruleAlertId: '123', + }, + }), + references: [ + { + name: 'param:alert_0', + id: '123', + type: 'alert', + }, + ], + }; + + expect(migration7160(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + legacyId: alert.id, + }, + references: [ + { + name: 'param:alert_0', + id: '123', + type: 'alert', + }, + ], + }); + }); + + test('security solution will not migrate "ruleAlertId" if it is invalid data', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.notifications', + params: { + ruleAlertId: 55, // This is invalid if it is a number and not a string + }, + }), + }; + + expect(migration7160(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + legacyId: alert.id, + }, + }); + }); + + test('security solution will not migrate "ruleAlertId" if it is invalid data but will keep existing references', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.notifications', + params: { + ruleAlertId: 456, // This is invalid if it is a number and not a string + }, + }), + references: [ + { + name: 'param:alert_0', + id: '123', + type: 'alert', + }, + ], + }; + + expect(migration7160(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + legacyId: alert.id, + }, + references: [ + { + name: 'param:alert_0', + id: '123', + type: 'alert', + }, + ], + }); + }); }); describe('8.0.0', () => { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index bd795b9efc61..9dcca5428527 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -54,6 +54,17 @@ export const isAnyActionSupportIncidents = (doc: SavedObjectUnsanitizedDoc): boolean => doc.attributes.alertTypeId === 'siem.signals'; +/** + * Returns true if the alert type is that of "siem.notifications" which is a legacy notification system that was deprecated in 7.16.0 + * in favor of using the newer alerting notifications system. + * @param doc The saved object alert type document + * @returns true if this is a legacy "siem.notifications" rule, otherwise false + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const isSecuritySolutionLegacyNotification = ( + doc: SavedObjectUnsanitizedDoc +): boolean => doc.attributes.alertTypeId === 'siem.notifications'; + export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, isPreconfigured: (connectorId: string) => boolean @@ -103,7 +114,11 @@ export function getMigrations( const migrateRules716 = createEsoMigration( encryptedSavedObjects, (doc): doc is SavedObjectUnsanitizedDoc => true, - pipeMigrations(setLegacyId, getRemovePreconfiguredConnectorsFromReferencesFn(isPreconfigured)) + pipeMigrations( + setLegacyId, + getRemovePreconfiguredConnectorsFromReferencesFn(isPreconfigured), + addRuleIdsToLegacyNotificationReferences + ) ); const migrationRules800 = createEsoMigration( @@ -574,6 +589,49 @@ function removeMalformedExceptionsList( } } +/** + * This migrates rule_id's within the legacy siem.notification to saved object references on an upgrade. + * We only migrate if we find these conditions: + * - ruleAlertId is a string and not null, undefined, or malformed data. + * - The existing references do not already have a ruleAlertId found within it. + * Some of these issues could crop up during either user manual errors of modifying things, earlier migration + * issues, etc... so we are safer to check them as possibilities + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + * @param doc The document that might have "ruleAlertId" to migrate into the references + * @returns The document migrated with saved object references + */ +function addRuleIdsToLegacyNotificationReferences( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + const { + attributes: { + params: { ruleAlertId }, + }, + references, + } = doc; + if (!isSecuritySolutionLegacyNotification(doc) || !isString(ruleAlertId)) { + // early return if we are not a string or if we are not a security solution notification saved object. + return doc; + } else { + const existingReferences = references ?? []; + const existingReferenceFound = existingReferences.find((reference) => { + return reference.id === ruleAlertId && reference.type === 'alert'; + }); + if (existingReferenceFound) { + // skip this if the references already exists for some uncommon reason so we do not add an additional one. + return doc; + } else { + const savedObjectReference: SavedObjectReference = { + id: ruleAlertId, + name: 'param:alert_0', + type: 'alert', + }; + const newReferences = [...existingReferences, savedObjectReference]; + return { ...doc, references: newReferences }; + } + } +} + function setLegacyId( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc { diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index c4658ae2ac22..2d1433324858 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -53,6 +53,8 @@ exports[`Error ERROR_EXC_TYPE 1`] = `undefined`; exports[`Error ERROR_GROUP_ID 1`] = `"grouping key"`; +exports[`Error ERROR_ID 1`] = `"error id"`; + exports[`Error ERROR_LOG_LEVEL 1`] = `undefined`; exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`; @@ -298,6 +300,8 @@ exports[`Span ERROR_EXC_TYPE 1`] = `undefined`; exports[`Span ERROR_GROUP_ID 1`] = `undefined`; +exports[`Span ERROR_ID 1`] = `undefined`; + exports[`Span ERROR_LOG_LEVEL 1`] = `undefined`; exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`; @@ -535,6 +539,8 @@ exports[`Transaction ERROR_EXC_TYPE 1`] = `undefined`; exports[`Transaction ERROR_GROUP_ID 1`] = `undefined`; +exports[`Transaction ERROR_ID 1`] = `undefined`; + exports[`Transaction ERROR_LOG_LEVEL 1`] = `undefined`; exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index d1f07c28bc80..4a4cad5454c4 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -78,6 +78,7 @@ export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM = // Parent ID for a transaction or span export const PARENT_ID = 'parent.id'; +export const ERROR_ID = 'error.id'; export const ERROR_GROUP_ID = 'error.grouping_key'; export const ERROR_CULPRIT = 'error.culprit'; export const ERROR_LOG_LEVEL = 'error.log.level'; diff --git a/x-pack/plugins/apm/common/processor_event.ts b/x-pack/plugins/apm/common/processor_event.ts index 57705e7ed4ce..fe0d9abfa0e5 100644 --- a/x-pack/plugins/apm/common/processor_event.ts +++ b/x-pack/plugins/apm/common/processor_event.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import * as t from 'io-ts'; export enum ProcessorEvent { transaction = 'transaction', @@ -12,6 +13,14 @@ export enum ProcessorEvent { span = 'span', profile = 'profile', } + +export const processorEventRt = t.union([ + t.literal(ProcessorEvent.transaction), + t.literal(ProcessorEvent.error), + t.literal(ProcessorEvent.metric), + t.literal(ProcessorEvent.span), + t.literal(ProcessorEvent.profile), +]); /** * Processor events that are searchable in the UI via the query bar. * diff --git a/x-pack/plugins/apm/dev_docs/apm_queries.md b/x-pack/plugins/apm/dev_docs/apm_queries.md new file mode 100644 index 000000000000..7d730d2ef2a7 --- /dev/null +++ b/x-pack/plugins/apm/dev_docs/apm_queries.md @@ -0,0 +1,426 @@ +# Transactions + +Transactions are stored in two different formats: + +#### Individual transactions document + +A single transaction with a latency of 2ms + +```json +{ + "@timestamp": "2021-09-01T10:00:00.000Z", + "processor.event": "transaction", + "transaction.duration.us": 2000, + "event.outcome": "success" +} +``` + +or + +#### Aggregated (metric) document +A pre-aggregated document where `_doc_count` is the number of original transactions, and `transaction.duration.histogram` is the latency distribution. + +```json +{ + "_doc_count": 2, + "@timestamp": "2021-09-01T10:00:00.000Z", + "processor.event": "metric", + "metricset.name": "transaction", + "transaction.duration.histogram": { + "counts": [1, 1], + "values": [2000, 3000] + }, + "event.outcome": "success" +} +``` + +The decision to use aggregated transactions or not is determined in [`getSearchAggregatedTransactions`](https://github.com/elastic/kibana/blob/a2ac439f56313b7a3fc4708f54a4deebf2615136/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts#L53-L79) and then used to [specify the index](https://github.com/elastic/kibana/blob/a2ac439f56313b7a3fc4708f54a4deebf2615136/x-pack/plugins/apm/server/lib/suggestions/get_suggestions.ts#L30-L32) and the [latency field](https://github.com/elastic/kibana/blob/a2ac439f56313b7a3fc4708f54a4deebf2615136/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts#L62-L65) + +### Latency + +Latency is the duration of a transaction. This can be calculated using transaction events or metric events (aggregated transactions). + +Noteworthy fields: `transaction.duration.us`, `transaction.duration.histogram` + +#### Transaction-based latency + +```json +{ + "size": 0, + "query": { + "bool": { + "filter": [{ "terms": { "processor.event": ["transaction"] } }] + } + }, + "aggs": { + "latency": { "avg": { "field": "transaction.duration.us" } } + } +} +``` + +#### Metric-based latency + +```json +{ + "size": 0, + "query": { + "bool": { + "filter": [ + { "terms": { "processor.event": ["metric"] } }, + { "term": { "metricset.name": "transaction" } } + ] + } + }, + "aggs": { + "latency": { "avg": { "field": "transaction.duration.histogram" } } + } +} +``` + +Please note: `metricset.name: transaction` was only recently introduced. To retain backwards compatability we still use the old filter `{ "exists": { "field": "transaction.duration.histogram" }}` when filtering for aggregated transactions. + +### Throughput + +Throughput is the number of transactions per minute. This can be calculated using transaction events or metric events (aggregated transactions). + +Noteworthy fields: None (based on `doc_count`) + +```js +{ + "size": 0, + "query": { + // same filters as for latency + }, + "aggs": { + "throughput": { "rate": { "unit": "minute" } } + } +} +``` + +### Failed transaction rate + +Failed transaction rate is the number of transactions with `event.outcome=failure` per minute. +Noteworthy fields: `event.outcome` + +```js +{ + "size": 0, + "query": { + // same filters as for latency + }, + "aggs": { + "outcomes": { + "terms": { + "field": "event.outcome", + "include": ["failure", "success"] + } + } + } +} +``` + +# System metrics + +System metrics are captured periodically (every 60 seconds by default). + +### CPU + +![image](https://user-images.githubusercontent.com/209966/135990500-f85bd8d9-b5a5-4b7c-b9e1-0759eefb8a29.png) + +Used in: [Metrics section](https://github.com/elastic/kibana/blob/00bb59713ed115343eb70d4e39059476edafbade/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts#L83) + +Noteworthy fields: `system.cpu.total.norm.pct`, `system.process.cpu.total.norm.pct` + +#### Sample document + +```json +{ + "@timestamp": "2021-09-01T10:00:00.000Z", + "processor.event": "metric", + "metricset.name": "app", + "system.process.cpu.total.norm.pct": 0.003, + "system.cpu.total.norm.pct": 0.28 +} +``` + +#### Query + +```json +{ + "size": 0, + "query": { + "bool": { + "filter": [ + { "terms": { "processor.event": ["metric"] } }, + { "terms": { "metricset.name": ["app"] } } + ] + } + }, + "aggs": { + "systemCPUAverage": { "avg": { "field": "system.cpu.total.norm.pct" } }, + "processCPUAverage": { + "avg": { "field": "system.process.cpu.total.norm.pct" } + } + } +} +``` + +### Memory + +![image](https://user-images.githubusercontent.com/209966/135990556-31716990-2812-46c3-a926-8c2a64c7c89f.png) + +Noteworthy fields: `system.memory.actual.free`, `system.memory.total`, + +#### Sample document + +```json +{ + "@timestamp": "2021-09-01T10:00:00.000Z", + "processor.event": "metric", + "metricset.name": "app", + "system.memory.actual.free": 13182939136, + "system.memory.total": 15735697408 +} +``` + +#### Query + +```js +{ + "size": 0, + "query": { + "bool": { + "filter": [ + { "terms": { "processor.event": ["metric"] }}, + { "terms": { "metricset.name": ["app"] }} + + // ensure the memory fields exists + { "exists": { "field": "system.memory.actual.free" }}, + { "exists": { "field": "system.memory.total" }}, + ] + } + }, + "aggs": { + "memoryUsedAvg": { + "avg": { + "script": { + "lang": "expression", + "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']" + } + } + } + } +} +``` + +Above example is overly simplified. In reality [we do a bit more](https://github.com/elastic/kibana/blob/fe9b5332e157fd456f81aecfd4ffa78d9e511a66/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts#L51-L71) to properly calculate memory usage inside containers + + + +# Transaction breakdown metrics (`transaction_breakdown`) + +A pre-aggregations of transaction documents where `transaction.breakdown.count` is the number of original transactions. + +Noteworthy fields: `transaction.name`, `transaction.type` + +#### Sample document + +```json +{ + "@timestamp": "2021-09-27T21:59:59.828Z", + "processor.event": "metric", + "metricset.name": "transaction_breakdown", + "transaction.breakdown.count": 12, + "transaction.name": "GET /api/products", + "transaction.type": "request" +} +} +``` + +# Span breakdown metrics (`span_breakdown`) + +A pre-aggregations of span documents where `span.self_time.count` is the number of original spans. Measures the "self-time" for a span type, and optional subtype, within a transaction group. + +Span breakdown metrics are used to power the "Time spent by span type" graph. Agents collect summarized metrics about the timings of spans, broken down by `span.type`. + +![image](https://user-images.githubusercontent.com/209966/135990865-9077ae3e-a7a4-4b5d-bdce-41dc832689ea.png) + +Used in: ["Time spent by span type" chart](https://github.com/elastic/kibana/blob/723370ab23573e50b3524a62c6b9998f2042423d/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts#L48-L87) + +Noteworthy fields: `transaction.name`, `transaction.type`, `span.type`, `span.subtype`, `span.self_time.*` + +#### Sample document + +```json +{ + "@timestamp": "2021-09-27T21:59:59.828Z", + "processor.event": "metric", + "metricset.name": "span_breakdown", + "transaction.name": "GET /api/products", + "transaction.type": "request", + "span.self_time.sum.us": 1028, + "span.self_time.count": 12, + "span.type": "db", + "span.subtype": "elasticsearch" +} +``` + +#### Query + +```json +{ + "size": 0, + "query": { + "bool": { + "filter": [ + { "terms": { "processor.event": ["metric"] } }, + { "terms": { "metricset.name": ["span_breakdown"] } } + ] + } + }, + "aggs": { + "total_self_time": { "sum": { "field": "span.self_time.sum.us" } }, + "types": { + "terms": { "field": "span.type" }, + "aggs": { + "subtypes": { + "terms": { "field": "span.subtype" }, + "aggs": { + "self_time_per_subtype": { + "sum": { "field": "span.self_time.sum.us" } + } + } + } + } + } + } +} +``` + +# Service destination metrics + +Pre-aggregations of span documents, where `span.destination.service.response_time.count` is the number of original spans. +These metrics measure the count and total duration of requests from one service to another service. + +![image](https://user-images.githubusercontent.com/209966/135990117-170070da-2fc5-4014-a597-0dda0970854c.png) + +Used in: [Dependencies (latency)](https://github.com/elastic/kibana/blob/00bb59713ed115343eb70d4e39059476edafbade/x-pack/plugins/apm/server/lib/backends/get_latency_charts_for_backend.ts#L68-L79), [Dependencies (throughput)](https://github.com/elastic/kibana/blob/00bb59713ed115343eb70d4e39059476edafbade/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts#L67-L74) and [Service Map](https://github.com/elastic/kibana/blob/00bb59713ed115343eb70d4e39059476edafbade/x-pack/plugins/apm/server/lib/service_map/get_service_map_backend_node_info.ts#L57-L67) + +Noteworthy fields: `span.destination.service.*` + +#### Sample document + +A pre-aggregated document with 73 span requests from opbeans-ruby to elasticsearch, and a combined latency of 1554ms + +```json +{ + "@timestamp": "2021-09-01T10:00:00.000Z", + "processor.event": "metric", + "metricset.name": "service_destination", + "service.name": "opbeans-ruby", + "span.destination.service.response_time.count": 73, + "span.destination.service.response_time.sum.us": 1554192, + "span.destination.service.resource": "elasticsearch", + "event.outcome": "success" +} +``` + +### Latency + +The latency between a service and an (external) endpoint + +```json +{ + "size": 0, + "query": { + "bool": { + "filter": [ + { "terms": { "processor.event": ["metric"] } }, + { "term": { "metricset.name": "service_destination" } }, + { "term": { "span.destination.service.resource": "elasticsearch" } } + ] + } + }, + "aggs": { + "latency_sum": { + "sum": { "field": "span.destination.service.response_time.sum.us" } + }, + "latency_count": { + "sum": { "field": "span.destination.service.response_time.count" } + } + } +} +``` + +### Throughput + +Captures the number of requests made from a service to an (external) endpoint + + +#### Query + +```json +{ + "size": 0, + "query": { + "bool": { + "filter": [ + { "terms": { "processor.event": ["metric"] } }, + { "term": { "metricset.name": "service_destination" } }, + { "term": { "span.destination.service.resource": "elasticsearch" } } + ] + } + }, + "aggs": { + "throughput": { + "rate": { + "field": "span.destination.service.response_time.count", + "unit": "minute" + } + } + } +} +``` + +## Common filters + +Most Elasticsearch queries will need to have one or more filters. There are a couple of reasons for adding filters: + +- correctness: Running an aggregation on unrelated documents will produce incorrect results +- stability: Running an aggregation on unrelated documents could cause the entire query to fail +- performance: limiting the number of documents will make the query faster + +```js +{ + "query": { + "bool": { + "filter": [ + // service name + { "term": { "service.name": "opbeans-go" }}, + + // service environment + { "term": { "service.environment": "testing" }} + + // transaction type + { "term": { "transaction.type": "request" }} + + // event type (possible values : transaction, span, metric, error) + { "terms": { "processor.event": ["metric"] }}, + + // metric set is a subtype of `processor.event: metric` + { "terms": { "metricset.name": ["transaction"] }}, + + // time range + { + "range": { + "@timestamp": { + "gte": 1633000560000, + "lte": 1633001498988, + "format": "epoch_millis" + } + } + } + ] + } + }, +``` diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx index 170e3a2fdad1..593de7c3a6f7 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -11,12 +11,12 @@ import { i18n } from '@kbn/i18n'; import { createExploratoryViewUrl, HeaderMenuPortal, - SeriesUrl, } from '../../../../../../observability/public'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AppMountParameters } from '../../../../../../../../src/core/public'; import { InspectorHeaderLink } from '../../../shared/apm_header_action_menu/inspector_header_link'; +import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; const ANALYZE_DATA = i18n.translate('xpack.apm.analyzeDataButtonLabel', { defaultMessage: 'Analyze data', @@ -39,15 +39,22 @@ export function UXActionMenu({ services: { http }, } = useKibana(); const { urlParams } = useUrlParams(); - const { rangeTo, rangeFrom } = urlParams; + const { rangeTo, rangeFrom, serviceName } = urlParams; const uxExploratoryViewLink = createExploratoryViewUrl( { - 'ux-series': { - dataType: 'ux', - isNew: true, - time: { from: rangeFrom, to: rangeTo }, - } as unknown as SeriesUrl, + reportType: 'kpi-over-time', + allSeries: [ + { + dataType: 'ux', + name: `${serviceName}-page-views`, + time: { from: rangeFrom!, to: rangeTo! }, + reportDefinitions: { + [SERVICE_NAME]: serviceName ? [serviceName] : [], + }, + selectedMetricField: 'Records', + }, + ], }, http?.basePath.get() ); @@ -61,6 +68,7 @@ export function UXActionMenu({ {ANALYZE_MESSAGE}

}> + j.job_id)} /> } title={i18n.translate( 'xpack.apm.settings.schema.migrationInProgressPanelTitle', - { defaultMessage: 'Switching to data streams...' } + { defaultMessage: 'Switching to Elastic Agent...' } )} description={i18n.translate( 'xpack.apm.settings.schema.migrationInProgressPanelDescription', diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx index ad6f315185cf..0031c102e8ae 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx @@ -7,7 +7,6 @@ import { EuiButton, - EuiCallOut, EuiCard, EuiFlexGroup, EuiFlexItem, @@ -86,7 +85,7 @@ export function SchemaOverview({ /> } title={i18n.translate('xpack.apm.settings.schema.success.title', { - defaultMessage: 'Data streams successfully setup!', + defaultMessage: 'Elastic Agent successfully setup!', })} description={i18n.translate( 'xpack.apm.settings.schema.success.description', @@ -139,17 +138,17 @@ export function SchemaOverview({ } + icon={} title={i18n.translate( 'xpack.apm.settings.schema.migrate.classicIndices.title', - { defaultMessage: 'Classic APM indices' } + { defaultMessage: 'APM Server binary' } )} display="subdued" description={i18n.translate( 'xpack.apm.settings.schema.migrate.classicIndices.description', { defaultMessage: - 'You are currently using classic APM indices for your data. This data schema is going away and is being replaced by data streams in Elastic Stack version 8.0.', + 'You are currently using APM Server binary. This legacy option is deprecated since version 7.16 and is being replaced by a managed APM Server in Elastic Agent from version 8.0.', } )} footer={ @@ -168,21 +167,6 @@ export function SchemaOverview({ rocket launch @@ -190,13 +174,13 @@ export function SchemaOverview({ } title={i18n.translate( 'xpack.apm.settings.schema.migrate.dataStreams.title', - { defaultMessage: 'Data streams' } + { defaultMessage: 'Elastic Agent' } )} description={i18n.translate( 'xpack.apm.settings.schema.migrate.dataStreams.description', { defaultMessage: - 'Going forward, any newly ingested data gets stored in data streams. Previously ingested data remains in classic APM indices. The APM and UX apps will continue to support both indices.', + 'Starting in version 8.0, Elastic Agent must manage APM Server. Elastic Agent can run on our hosted Elasticsearch Service, ECE, or be self-managed. Then, add the Elastic APM integration to continue ingesting APM data.', } )} footer={ @@ -216,7 +200,7 @@ export function SchemaOverview({ > {i18n.translate( 'xpack.apm.settings.schema.migrate.dataStreams.buttonText', - { defaultMessage: 'Switch to data streams' } + { defaultMessage: 'Switch to Elastic Agent' } )}
@@ -238,7 +222,7 @@ export function SchemaOverviewHeading() { @@ -256,15 +240,15 @@ export function SchemaOverviewHeading() { )} ), - dataStreamsDocLink: ( + elasticAgentDocLink: ( {i18n.translate( - 'xpack.apm.settings.schema.descriptionText.dataStreamsDocLinkText', - { defaultMessage: 'data streams' } + 'xpack.apm.settings.schema.descriptionText.elasticAgentDocLinkText', + { defaultMessage: 'Elastic Agent' } )} ), @@ -272,30 +256,6 @@ export function SchemaOverviewHeading() { /> - - - - - {i18n.translate( - 'xpack.apm.settings.schema.descriptionText.betaCalloutMessage', - { - defaultMessage: - 'This functionality is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features.', - } - )} - - - - - ); } @@ -338,7 +298,7 @@ function getDisabledReason({ return ( diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx index 08cebbe1880e..f499cf88ecdb 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx @@ -38,7 +38,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:ux,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:ux,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' ); }); }); @@ -48,7 +48,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' ); }); }); @@ -58,7 +58,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ENVIRONMENT_NOT_DEFINED),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' ); }); }); @@ -68,7 +68,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' ); }); }); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx index 068d7bb1c242..a4fc964a444c 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx @@ -9,10 +9,7 @@ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { - createExploratoryViewUrl, - SeriesUrl, -} from '../../../../../../observability/public'; +import { createExploratoryViewUrl } from '../../../../../../observability/public'; import { ALL_VALUES_SELECTED } from '../../../../../../observability/public'; import { isIosAgentName, @@ -21,6 +18,7 @@ import { import { SERVICE_ENVIRONMENT, SERVICE_NAME, + TRANSACTION_DURATION, } from '../../../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, @@ -29,13 +27,11 @@ import { import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useApmParams } from '../../../../hooks/use_apm_params'; -function getEnvironmentDefinition(environment?: string) { +function getEnvironmentDefinition(environment: string) { switch (environment) { case ENVIRONMENT_ALL.value: return { [SERVICE_ENVIRONMENT]: [ALL_VALUES_SELECTED] }; case ENVIRONMENT_NOT_DEFINED.value: - case undefined: - return {}; default: return { [SERVICE_ENVIRONMENT]: [environment] }; } @@ -54,21 +50,26 @@ export function AnalyzeDataButton() { if ( (isRumAgentName(agentName) || isIosAgentName(agentName)) && - canShowDashboard + rangeFrom && + canShowDashboard && + rangeTo ) { const href = createExploratoryViewUrl( { - 'apm-series': { - dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', - time: { from: rangeFrom, to: rangeTo }, - reportType: 'kpi-over-time', - reportDefinitions: { - [SERVICE_NAME]: [serviceName], - ...getEnvironmentDefinition(environment), + reportType: 'kpi-over-time', + allSeries: [ + { + name: `${serviceName}-response-latency`, + selectedMetricField: TRANSACTION_DURATION, + dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', + time: { from: rangeFrom, to: rangeTo }, + reportDefinitions: { + [SERVICE_NAME]: [serviceName], + ...(environment ? getEnvironmentDefinition(environment) : {}), + }, + operationType: 'average', }, - operationType: 'average', - isNew: true, - } as SeriesUrl, + ], }, basepath ); diff --git a/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx index 13aa3696eda4..e9525728bc3c 100644 --- a/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { castArray } from 'lodash'; import React, { TableHTMLAttributes } from 'react'; import { EuiTable, @@ -26,16 +26,32 @@ export function KeyValueTable({ return ( - {keyValuePairs.map(({ key, value }) => ( - - - {key} - - - - - - ))} + {keyValuePairs.map(({ key, value }) => { + const asArray = castArray(value); + const valueList = + asArray.length <= 1 ? ( + + ) : ( +
    + {asArray.map((val, index) => ( +
  • + +
  • + ))} +
+ ); + + return ( + + + {key} + + + {valueList} + + + ); + })}
); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx deleted file mode 100644 index f936941923e4..000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { ErrorMetadata } from '.'; -import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; -import { - expectTextsInDocument, - expectTextsNotInDocument, -} from '../../../../utils/testHelpers'; - -function Wrapper({ children }: { children?: ReactNode }) { - return ( - - {children} - - ); -} - -const renderOptions = { - wrapper: Wrapper, -}; - -function getError() { - return { - labels: { someKey: 'labels value' }, - http: { someKey: 'http value' }, - host: { someKey: 'host value' }, - container: { someKey: 'container value' }, - service: { someKey: 'service value' }, - process: { someKey: 'process value' }, - agent: { someKey: 'agent value' }, - url: { someKey: 'url value' }, - user: { someKey: 'user value' }, - notIncluded: 'not included value', - error: { - id: '7efbc7056b746fcb', - notIncluded: 'error not included value', - custom: { - someKey: 'custom value', - }, - }, - } as unknown as APMError; -} - -describe('ErrorMetadata', () => { - it('should render a error with all sections', () => { - const error = getError(); - const output = render(, renderOptions); - - // sections - expectTextsInDocument(output, [ - 'Labels', - 'HTTP', - 'Host', - 'Container', - 'Service', - 'Process', - 'Agent', - 'URL', - 'User', - 'Custom', - ]); - }); - - it('should render a error with all included dot notation keys', () => { - const error = getError(); - const output = render(, renderOptions); - - // included keys - expectTextsInDocument(output, [ - 'labels.someKey', - 'http.someKey', - 'host.someKey', - 'container.someKey', - 'service.someKey', - 'process.someKey', - 'agent.someKey', - 'url.someKey', - 'user.someKey', - 'error.custom.someKey', - ]); - - // excluded keys - expectTextsNotInDocument(output, ['notIncluded', 'error.notIncluded']); - }); - - it('should render a error with all included values', () => { - const error = getError(); - const output = render(, renderOptions); - - // included values - expectTextsInDocument(output, [ - 'labels value', - 'http value', - 'host value', - 'container value', - 'service value', - 'process value', - 'agent value', - 'url value', - 'user value', - 'custom value', - ]); - - // excluded values - expectTextsNotInDocument(output, [ - 'not included value', - 'error not included value', - ]); - }); - - it('should render a error with only the required sections', () => { - const error = {} as APMError; - const output = render(, renderOptions); - - // required sections should be found - expectTextsInDocument(output, ['Labels', 'User']); - - // optional sections should NOT be found - expectTextsNotInDocument(output, [ - 'HTTP', - 'Host', - 'Container', - 'Service', - 'Process', - 'Agent', - 'URL', - 'Custom', - ]); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx index 196a8706d513..f6ffc34ecee0 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx @@ -6,19 +6,41 @@ */ import React, { useMemo } from 'react'; -import { ERROR_METADATA_SECTIONS } from './sections'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { getSectionsWithRows } from '../helper'; +import { getSectionsFromFields } from '../helper'; import { MetadataTable } from '..'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { ProcessorEvent } from '../../../../../common/processor_event'; interface Props { error: APMError; } export function ErrorMetadata({ error }: Props) { - const sectionsWithRows = useMemo( - () => getSectionsWithRows(ERROR_METADATA_SECTIONS, error), - [error] + const { data: errorEvent, status } = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.error, + id: error.error.id, + }, + }, + }); + }, + [error.error.id] + ); + + const sections = useMemo( + () => getSectionsFromFields(errorEvent?.metadata || {}), + [errorEvent?.metadata] + ); + + return ( + ); - return ; } diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts deleted file mode 100644 index 28a64ac36660..000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Section, - ERROR, - LABELS, - HTTP, - HOST, - CONTAINER, - SERVICE, - PROCESS, - AGENT, - URL, - USER, - CUSTOM_ERROR, - TRACE, - TRANSACTION, -} from '../sections'; - -export const ERROR_METADATA_SECTIONS: Section[] = [ - { ...LABELS, required: true }, - TRACE, - TRANSACTION, - ERROR, - HTTP, - HOST, - CONTAINER, - SERVICE, - PROCESS, - AGENT, - URL, - { ...USER, required: true }, - CUSTOM_ERROR, -]; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx index 7ccde6a9a74d..5d5976866ba2 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx @@ -11,7 +11,7 @@ import { MemoryRouter } from 'react-router-dom'; import { MetadataTable } from '.'; import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument } from '../../../utils/testHelpers'; -import { SectionsWithRows } from './helper'; +import type { SectionDescriptor } from './types'; function Wrapper({ children }: { children?: ReactNode }) { return ( @@ -27,21 +27,20 @@ const renderOptions = { describe('MetadataTable', () => { it('shows sections', () => { - const sectionsWithRows = [ - { key: 'foo', label: 'Foo', required: true }, + const sections: SectionDescriptor[] = [ + { key: 'foo', label: 'Foo', required: true, properties: [] }, { key: 'bar', label: 'Bar', required: false, - properties: ['props.A', 'props.B'], - rows: [ - { key: 'props.A', value: 'A' }, - { key: 'props.B', value: 'B' }, + properties: [ + { field: 'props.A', value: ['A'] }, + { field: 'props.B', value: ['B'] }, ], }, - ] as unknown as SectionsWithRows; + ]; const output = render( - , + , renderOptions ); expectTextsInDocument(output, [ @@ -56,15 +55,17 @@ describe('MetadataTable', () => { }); describe('required sections', () => { it('shows "empty state message" if no data is available', () => { - const sectionsWithRows = [ + const sectionsWithRows: SectionDescriptor[] = [ { key: 'foo', label: 'Foo', required: true, + properties: [], }, - ] as unknown as SectionsWithRows; + ]; + const output = render( - , + , renderOptions ); expectTextsInDocument(output, ['Foo', 'No data available']); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx index d44464e2160d..ed816b1c7a33 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx @@ -12,7 +12,7 @@ import { expectTextsInDocument } from '../../../utils/testHelpers'; describe('Section', () => { it('shows "empty state message" if no data is available', () => { - const component = render(
); + const component = render(
); expectTextsInDocument(component, ['No data available']); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx index ff86083b8612..03ae237f470c 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx @@ -10,15 +10,21 @@ import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; import { KeyValueTable } from '../KeyValueTable'; -import { KeyValuePair } from '../../../utils/flattenObject'; interface Props { - keyValuePairs: KeyValuePair[]; + properties: Array<{ field: string; value: string[] | number[] }>; } -export function Section({ keyValuePairs }: Props) { - if (!isEmpty(keyValuePairs)) { - return ; +export function Section({ properties }: Props) { + if (!isEmpty(properties)) { + return ( + ({ + key: property.field, + value: property.value, + }))} + /> + ); } return ( diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx deleted file mode 100644 index 46eaba1e9e11..000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { SpanMetadata } from '.'; -import { Span } from '../../../../../typings/es_schemas/ui/span'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; -import { - expectTextsInDocument, - expectTextsNotInDocument, -} from '../../../../utils/testHelpers'; - -function Wrapper({ children }: { children?: ReactNode }) { - return ( - - {children} - - ); -} - -const renderOptions = { - wrapper: Wrapper, -}; - -describe('SpanMetadata', () => { - describe('render', () => { - it('renders', () => { - const span = { - agent: { - ephemeral_id: 'ed8e3a4f-21d2-4a1f-bbc7-fa2064d94225', - name: 'java', - version: '1.9.1-SNAPSHOT', - }, - service: { - name: 'opbeans-java', - }, - span: { - id: '7efbc7056b746fcb', - message: { - age: { ms: 1577958057123 }, - queue: { name: 'queue name' }, - }, - }, - } as unknown as Span; - const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent', 'Message']); - }); - }); - describe('when a span is presented', () => { - it('renders the span', () => { - const span = { - agent: { - ephemeral_id: 'ed8e3a4f-21d2-4a1f-bbc7-fa2064d94225', - name: 'java', - version: '1.9.1-SNAPSHOT', - }, - service: { - name: 'opbeans-java', - }, - span: { - id: '7efbc7056b746fcb', - http: { - response: { status_code: 200 }, - }, - subtype: 'http', - type: 'external', - message: { - age: { ms: 1577958057123 }, - queue: { name: 'queue name' }, - }, - }, - } as unknown as Span; - const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent', 'Span', 'Message']); - }); - }); - describe('when there is no id inside span', () => { - it('does not show the section', () => { - const span = { - agent: { - ephemeral_id: 'ed8e3a4f-21d2-4a1f-bbc7-fa2064d94225', - name: 'java', - version: '1.9.1-SNAPSHOT', - }, - service: { - name: 'opbeans-java', - }, - span: { - http: { - response: { status_code: 200 }, - }, - subtype: 'http', - type: 'external', - }, - } as unknown as Span; - const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent']); - expectTextsNotInDocument(output, ['Span', 'Message']); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx index feefcea9d38a..bf5702b4acf3 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx @@ -6,19 +6,41 @@ */ import React, { useMemo } from 'react'; -import { SPAN_METADATA_SECTIONS } from './sections'; import { Span } from '../../../../../typings/es_schemas/ui/span'; -import { getSectionsWithRows } from '../helper'; +import { getSectionsFromFields } from '../helper'; import { MetadataTable } from '..'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { ProcessorEvent } from '../../../../../common/processor_event'; interface Props { span: Span; } export function SpanMetadata({ span }: Props) { - const sectionsWithRows = useMemo( - () => getSectionsWithRows(SPAN_METADATA_SECTIONS, span), - [span] + const { data: spanEvent, status } = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.span, + id: span.span.id, + }, + }, + }); + }, + [span.span.id] + ); + + const sections = useMemo( + () => getSectionsFromFields(spanEvent?.metadata || {}), + [spanEvent?.metadata] + ); + + return ( + ); - return ; } diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts deleted file mode 100644 index f19aef8e0bd8..000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Section, - AGENT, - SERVICE, - SPAN, - LABELS, - EVENT, - TRANSACTION, - TRACE, - MESSAGE_SPAN, -} from '../sections'; - -export const SPAN_METADATA_SECTIONS: Section[] = [ - LABELS, - TRACE, - TRANSACTION, - EVENT, - SPAN, - SERVICE, - MESSAGE_SPAN, - AGENT, -]; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx deleted file mode 100644 index 08253f04777d..000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { TransactionMetadata } from '.'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; -import { - expectTextsInDocument, - expectTextsNotInDocument, -} from '../../../../utils/testHelpers'; - -function Wrapper({ children }: { children?: ReactNode }) { - return ( - - {children} - - ); -} - -const renderOptions = { - wrapper: Wrapper, -}; - -function getTransaction() { - return { - labels: { someKey: 'labels value' }, - http: { someKey: 'http value' }, - host: { someKey: 'host value' }, - container: { someKey: 'container value' }, - service: { someKey: 'service value' }, - process: { someKey: 'process value' }, - agent: { someKey: 'agent value' }, - url: { someKey: 'url value' }, - user: { someKey: 'user value' }, - notIncluded: 'not included value', - transaction: { - id: '7efbc7056b746fcb', - notIncluded: 'transaction not included value', - custom: { - someKey: 'custom value', - }, - message: { - age: { ms: 1577958057123 }, - queue: { name: 'queue name' }, - }, - }, - } as unknown as Transaction; -} - -describe('TransactionMetadata', () => { - it('should render a transaction with all sections', () => { - const transaction = getTransaction(); - const output = render( - , - renderOptions - ); - - // sections - expectTextsInDocument(output, [ - 'Labels', - 'HTTP', - 'Host', - 'Container', - 'Service', - 'Process', - 'Agent', - 'URL', - 'User', - 'Custom', - 'Message', - ]); - }); - - it('should render a transaction with all included dot notation keys', () => { - const transaction = getTransaction(); - const output = render( - , - renderOptions - ); - - // included keys - expectTextsInDocument(output, [ - 'labels.someKey', - 'http.someKey', - 'host.someKey', - 'container.someKey', - 'service.someKey', - 'process.someKey', - 'agent.someKey', - 'url.someKey', - 'user.someKey', - 'transaction.custom.someKey', - 'transaction.message.age.ms', - 'transaction.message.queue.name', - ]); - - // excluded keys - expectTextsNotInDocument(output, [ - 'notIncluded', - 'transaction.notIncluded', - ]); - }); - - it('should render a transaction with all included values', () => { - const transaction = getTransaction(); - const output = render( - , - renderOptions - ); - - // included values - expectTextsInDocument(output, [ - 'labels value', - 'http value', - 'host value', - 'container value', - 'service value', - 'process value', - 'agent value', - 'url value', - 'user value', - 'custom value', - '1577958057123', - 'queue name', - ]); - - // excluded values - expectTextsNotInDocument(output, [ - 'not included value', - 'transaction not included value', - ]); - }); - - it('should render a transaction with only the required sections', () => { - const transaction = {} as Transaction; - const output = render( - , - renderOptions - ); - - // required sections should be found - expectTextsInDocument(output, ['Labels', 'User']); - - // optional sections should NOT be found - expectTextsNotInDocument(output, [ - 'HTTP', - 'Host', - 'Container', - 'Service', - 'Process', - 'Agent', - 'URL', - 'Custom', - 'Message', - ]); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx index b3a53472f081..32c0101c73b4 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx @@ -6,19 +6,40 @@ */ import React, { useMemo } from 'react'; -import { TRANSACTION_METADATA_SECTIONS } from './sections'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { getSectionsWithRows } from '../helper'; +import { getSectionsFromFields } from '../helper'; import { MetadataTable } from '..'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; interface Props { transaction: Transaction; } export function TransactionMetadata({ transaction }: Props) { - const sectionsWithRows = useMemo( - () => getSectionsWithRows(TRANSACTION_METADATA_SECTIONS, transaction), - [transaction] + const { data: transactionEvent, status } = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.transaction, + id: transaction.transaction.id, + }, + }, + }); + }, + [transaction.transaction.id] + ); + + const sections = useMemo( + () => getSectionsFromFields(transactionEvent?.metadata || {}), + [transactionEvent?.metadata] + ); + return ( + ); - return ; } diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts deleted file mode 100644 index 2f4a3d322985..000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Section, - TRANSACTION, - LABELS, - EVENT, - HTTP, - HOST, - CLIENT, - CONTAINER, - SERVICE, - PROCESS, - AGENT, - URL, - PAGE, - USER, - USER_AGENT, - CUSTOM_TRANSACTION, - MESSAGE_TRANSACTION, - TRACE, -} from '../sections'; - -export const TRANSACTION_METADATA_SECTIONS: Section[] = [ - { ...LABELS, required: true }, - TRACE, - TRANSACTION, - EVENT, - HTTP, - HOST, - CLIENT, - CONTAINER, - SERVICE, - PROCESS, - MESSAGE_TRANSACTION, - AGENT, - URL, - { ...PAGE, key: 'transaction.page' }, - { ...USER, required: true }, - USER_AGENT, - CUSTOM_TRANSACTION, -]; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts index 770b35e7d17f..2e64c170437d 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts @@ -5,62 +5,52 @@ * 2.0. */ -import { getSectionsWithRows, filterSectionsByTerm } from './helper'; -import { LABELS, HTTP, SERVICE } from './sections'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; +import { filterSectionsByTerm, getSectionsFromFields } from './helper'; describe('MetadataTable Helper', () => { - const sections = [ - { ...LABELS, required: true }, - HTTP, - { ...SERVICE, properties: ['environment'] }, - ]; - const apmDoc = { - http: { - headers: { - Connection: 'close', - Host: 'opbeans:3000', - request: { method: 'get' }, - }, - }, - service: { - framework: { name: 'express' }, - environment: 'production', - }, - } as unknown as Transaction; - const metadataItems = getSectionsWithRows(sections, apmDoc); + const fields = { + 'http.headers.Connection': ['close'], + 'http.headers.Host': ['opbeans:3000'], + 'http.headers.request.method': ['get'], + 'service.framework.name': ['express'], + 'service.environment': ['production'], + }; + + const metadataItems = getSectionsFromFields(fields); - it('returns flattened data and required section', () => { + it('returns flattened data', () => { expect(metadataItems).toEqual([ - { key: 'labels', label: 'Labels', required: true, rows: [] }, { key: 'http', - label: 'HTTP', - rows: [ - { key: 'http.headers.Connection', value: 'close' }, - { key: 'http.headers.Host', value: 'opbeans:3000' }, - { key: 'http.headers.request.method', value: 'get' }, + label: 'http', + properties: [ + { field: 'http.headers.Connection', value: ['close'] }, + { field: 'http.headers.Host', value: ['opbeans:3000'] }, + { field: 'http.headers.request.method', value: ['get'] }, ], }, { key: 'service', - label: 'Service', - properties: ['environment'], - rows: [{ key: 'service.environment', value: 'production' }], + label: 'service', + properties: [ + { field: 'service.environment', value: ['production'] }, + { field: 'service.framework.name', value: ['express'] }, + ], }, ]); }); + describe('filter', () => { it('items by key', () => { const filteredItems = filterSectionsByTerm(metadataItems, 'http'); expect(filteredItems).toEqual([ { key: 'http', - label: 'HTTP', - rows: [ - { key: 'http.headers.Connection', value: 'close' }, - { key: 'http.headers.Host', value: 'opbeans:3000' }, - { key: 'http.headers.request.method', value: 'get' }, + label: 'http', + properties: [ + { field: 'http.headers.Connection', value: ['close'] }, + { field: 'http.headers.Host', value: ['opbeans:3000'] }, + { field: 'http.headers.request.method', value: ['get'] }, ], }, ]); @@ -71,9 +61,8 @@ describe('MetadataTable Helper', () => { expect(filteredItems).toEqual([ { key: 'service', - label: 'Service', - properties: ['environment'], - rows: [{ key: 'service.environment', value: 'production' }], + label: 'service', + properties: [{ field: 'service.environment', value: ['production'] }], }, ]); }); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts index bd115c1c7c17..c9e0f2aa6674 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts @@ -5,35 +5,52 @@ * 2.0. */ -import { get, pick, isEmpty } from 'lodash'; -import { Section } from './sections'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; -import { APMError } from '../../../../typings/es_schemas/ui/apm_error'; -import { Span } from '../../../../typings/es_schemas/ui/span'; -import { flattenObject, KeyValuePair } from '../../../utils/flattenObject'; - -export type SectionsWithRows = ReturnType; - -export const getSectionsWithRows = ( - sections: Section[], - apmDoc: Transaction | APMError | Span -) => { - return sections - .map((section) => { - const sectionData: Record = get(apmDoc, section.key); - const filteredData: Record | undefined = - section.properties - ? pick(sectionData, section.properties) - : sectionData; - - const rows: KeyValuePair[] = flattenObject(filteredData, section.key); - return { ...section, rows }; - }) - .filter(({ required, rows }) => required || !isEmpty(rows)); +import { isEmpty, groupBy, partition } from 'lodash'; +import type { SectionDescriptor } from './types'; + +const EXCLUDED_FIELDS = ['error.exception.stacktrace', 'span.stacktrace']; + +export const getSectionsFromFields = (fields: Record) => { + const rows = Object.keys(fields) + .filter( + (field) => !EXCLUDED_FIELDS.some((excluded) => field.startsWith(excluded)) + ) + .sort() + .map((field) => { + return { + section: field.split('.')[0], + field, + value: fields[field], + }; + }); + + const sections = Object.values(groupBy(rows, 'section')).map( + (rowsForSection) => { + const first = rowsForSection[0]; + + const section: SectionDescriptor = { + key: first.section, + label: first.section.toLowerCase(), + properties: rowsForSection.map((row) => ({ + field: row.field, + value: row.value, + })), + }; + + return section; + } + ); + + const [labelSections, otherSections] = partition( + sections, + (section) => section.key === 'labels' + ); + + return [...labelSections, ...otherSections]; }; export const filterSectionsByTerm = ( - sections: SectionsWithRows, + sections: SectionDescriptor[], searchTerm: string ) => { if (!searchTerm) { @@ -41,15 +58,16 @@ export const filterSectionsByTerm = ( } return sections .map((section) => { - const { rows = [] } = section; - const filteredRows = rows.filter(({ key, value }) => { - const valueAsString = String(value).toLowerCase(); + const { properties = [] } = section; + const filteredProps = properties.filter(({ field, value }) => { return ( - key.toLowerCase().includes(searchTerm) || - valueAsString.includes(searchTerm) + field.toLowerCase().includes(searchTerm) || + value.some((val: string | number) => + String(val).toLowerCase().includes(searchTerm) + ) ); }); - return { ...section, rows: filteredRows }; + return { ...section, properties: filteredProps }; }) - .filter(({ rows }) => !isEmpty(rows)); + .filter(({ properties }) => !isEmpty(properties)); }; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx index 45be525512d0..248fa240fd55 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx @@ -19,18 +19,21 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useCallback } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { HeightRetainer } from '../HeightRetainer'; import { fromQuery, toQuery } from '../Links/url_helpers'; -import { filterSectionsByTerm, SectionsWithRows } from './helper'; +import { filterSectionsByTerm } from './helper'; import { Section } from './Section'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { SectionDescriptor } from './types'; interface Props { - sections: SectionsWithRows; + sections: SectionDescriptor[]; + isLoading: boolean; } -export function MetadataTable({ sections }: Props) { +export function MetadataTable({ sections, isLoading }: Props) { const history = useHistory(); const location = useLocation(); const { urlParams } = useUrlParams(); @@ -77,6 +80,13 @@ export function MetadataTable({ sections }: Props) { /> + {isLoading && ( + + + + + + )} {filteredSections.map((section) => (
@@ -84,7 +94,7 @@ export function MetadataTable({ sections }: Props) {
{section.label}
-
+
))} diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts deleted file mode 100644 index efc2ef8bde66..000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export interface Section { - key: string; - label: string; - required?: boolean; - properties?: string[]; -} - -export const LABELS: Section = { - key: 'labels', - label: i18n.translate('xpack.apm.metadataTable.section.labelsLabel', { - defaultMessage: 'Labels', - }), -}; - -export const EVENT: Section = { - key: 'event', - label: i18n.translate('xpack.apm.metadataTable.section.eventLabel', { - defaultMessage: 'event', - }), - properties: ['outcome'], -}; - -export const HTTP: Section = { - key: 'http', - label: i18n.translate('xpack.apm.metadataTable.section.httpLabel', { - defaultMessage: 'HTTP', - }), -}; - -export const HOST: Section = { - key: 'host', - label: i18n.translate('xpack.apm.metadataTable.section.hostLabel', { - defaultMessage: 'Host', - }), -}; - -export const CLIENT: Section = { - key: 'client', - label: i18n.translate('xpack.apm.metadataTable.section.clientLabel', { - defaultMessage: 'Client', - }), - properties: ['ip'], -}; - -export const CONTAINER: Section = { - key: 'container', - label: i18n.translate('xpack.apm.metadataTable.section.containerLabel', { - defaultMessage: 'Container', - }), -}; - -export const SERVICE: Section = { - key: 'service', - label: i18n.translate('xpack.apm.metadataTable.section.serviceLabel', { - defaultMessage: 'Service', - }), -}; - -export const PROCESS: Section = { - key: 'process', - label: i18n.translate('xpack.apm.metadataTable.section.processLabel', { - defaultMessage: 'Process', - }), -}; - -export const AGENT: Section = { - key: 'agent', - label: i18n.translate('xpack.apm.metadataTable.section.agentLabel', { - defaultMessage: 'Agent', - }), -}; - -export const URL: Section = { - key: 'url', - label: i18n.translate('xpack.apm.metadataTable.section.urlLabel', { - defaultMessage: 'URL', - }), -}; - -export const USER: Section = { - key: 'user', - label: i18n.translate('xpack.apm.metadataTable.section.userLabel', { - defaultMessage: 'User', - }), -}; - -export const USER_AGENT: Section = { - key: 'user_agent', - label: i18n.translate('xpack.apm.metadataTable.section.userAgentLabel', { - defaultMessage: 'User agent', - }), -}; - -export const PAGE: Section = { - key: 'page', - label: i18n.translate('xpack.apm.metadataTable.section.pageLabel', { - defaultMessage: 'Page', - }), -}; - -export const SPAN: Section = { - key: 'span', - label: i18n.translate('xpack.apm.metadataTable.section.spanLabel', { - defaultMessage: 'Span', - }), - properties: ['id'], -}; - -export const TRANSACTION: Section = { - key: 'transaction', - label: i18n.translate('xpack.apm.metadataTable.section.transactionLabel', { - defaultMessage: 'Transaction', - }), - properties: ['id'], -}; - -export const TRACE: Section = { - key: 'trace', - label: i18n.translate('xpack.apm.metadataTable.section.traceLabel', { - defaultMessage: 'Trace', - }), - properties: ['id'], -}; - -export const ERROR: Section = { - key: 'error', - label: i18n.translate('xpack.apm.metadataTable.section.errorLabel', { - defaultMessage: 'Error', - }), - properties: ['id'], -}; - -const customLabel = i18n.translate( - 'xpack.apm.metadataTable.section.customLabel', - { - defaultMessage: 'Custom', - } -); - -export const CUSTOM_ERROR: Section = { - key: 'error.custom', - label: customLabel, -}; -export const CUSTOM_TRANSACTION: Section = { - key: 'transaction.custom', - label: customLabel, -}; - -const messageLabel = i18n.translate( - 'xpack.apm.metadataTable.section.messageLabel', - { - defaultMessage: 'Message', - } -); - -export const MESSAGE_TRANSACTION: Section = { - key: 'transaction.message', - label: messageLabel, -}; - -export const MESSAGE_SPAN: Section = { - key: 'span.message', - label: messageLabel, -}; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/types.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/types.ts new file mode 100644 index 000000000000..3ce7698460f3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface SectionDescriptor { + key: string; + label: string; + required?: boolean; + properties: Array<{ field: string; value: string[] | number[] }>; +} diff --git a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts new file mode 100644 index 000000000000..d706146faf21 --- /dev/null +++ b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GetDeprecationsContext } from '../../../../../src/core/server'; +import { CloudSetup } from '../../../cloud/server'; +import { getDeprecations } from './'; +import { APMRouteHandlerResources } from '../'; +import { AgentPolicy } from '../../../fleet/common'; + +const deprecationContext = { + esClient: {}, + savedObjectsClient: {}, +} as GetDeprecationsContext; + +describe('getDeprecations', () => { + describe('when fleet is disabled', () => { + it('returns no deprecations', async () => { + const deprecationsCallback = getDeprecations({}); + const deprecations = await deprecationsCallback(deprecationContext); + expect(deprecations).toEqual([]); + }); + }); + + describe('when running on cloud with legacy apm-server', () => { + it('returns deprecations', async () => { + const deprecationsCallback = getDeprecations({ + cloudSetup: { isCloudEnabled: true } as unknown as CloudSetup, + fleet: { + start: () => ({ + agentPolicyService: { get: () => undefined }, + }), + } as unknown as APMRouteHandlerResources['plugins']['fleet'], + }); + const deprecations = await deprecationsCallback(deprecationContext); + expect(deprecations).not.toEqual([]); + }); + }); + + describe('when running on cloud with fleet', () => { + it('returns no deprecations', async () => { + const deprecationsCallback = getDeprecations({ + cloudSetup: { isCloudEnabled: true } as unknown as CloudSetup, + fleet: { + start: () => ({ + agentPolicyService: { get: () => ({ id: 'foo' } as AgentPolicy) }, + }), + } as unknown as APMRouteHandlerResources['plugins']['fleet'], + }); + const deprecations = await deprecationsCallback(deprecationContext); + expect(deprecations).toEqual([]); + }); + }); + + describe('when running on prem', () => { + it('returns no deprecations', async () => { + const deprecationsCallback = getDeprecations({ + cloudSetup: { isCloudEnabled: false } as unknown as CloudSetup, + fleet: { + start: () => ({ agentPolicyService: { get: () => undefined } }), + } as unknown as APMRouteHandlerResources['plugins']['fleet'], + }); + const deprecations = await deprecationsCallback(deprecationContext); + expect(deprecations).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/deprecations/index.ts b/x-pack/plugins/apm/server/deprecations/index.ts new file mode 100644 index 000000000000..b592a2bf1326 --- /dev/null +++ b/x-pack/plugins/apm/server/deprecations/index.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GetDeprecationsContext, DeprecationsDetails } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { CloudSetup } from '../../../cloud/server'; +import { getCloudAgentPolicy } from '../lib/fleet/get_cloud_apm_package_policy'; +import { APMRouteHandlerResources } from '../'; + +export function getDeprecations({ + cloudSetup, + fleet, +}: { + cloudSetup?: CloudSetup; + fleet?: APMRouteHandlerResources['plugins']['fleet']; +}) { + return async ({ + savedObjectsClient, + }: GetDeprecationsContext): Promise => { + const deprecations: DeprecationsDetails[] = []; + if (!fleet) { + return deprecations; + } + + const fleetPluginStart = await fleet.start(); + const cloudAgentPolicy = await getCloudAgentPolicy({ + fleetPluginStart, + savedObjectsClient, + }); + + const isCloudEnabled = !!cloudSetup?.isCloudEnabled; + + const hasCloudAgentPolicy = !isEmpty(cloudAgentPolicy); + + if (isCloudEnabled && !hasCloudAgentPolicy) { + deprecations.push({ + title: i18n.translate('xpack.apm.deprecations.legacyModeTitle', { + defaultMessage: 'APM Server running in legacy mode', + }), + message: i18n.translate('xpack.apm.deprecations.message', { + defaultMessage: + 'Running the APM Server binary directly is considered a legacy option and is deprecated since 7.16. Switch to APM Server managed by an Elastic Agent instead. Read our documentation to learn more.', + }), + documentationUrl: + 'https://www.elastic.co/guide/en/apm/server/current/apm-integration.html', + level: 'warning', + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.apm.deprecations.steps.apm', { + defaultMessage: 'Navigate to Observability/APM', + }), + i18n.translate('xpack.apm.deprecations.steps.settings', { + defaultMessage: 'Click on "Settings"', + }), + i18n.translate('xpack.apm.deprecations.steps.schema', { + defaultMessage: 'Select "Schema" tab', + }), + i18n.translate('xpack.apm.deprecations.steps.switch', { + defaultMessage: + 'Click "Switch to data streams". You will be guided through the process', + }), + ], + }, + }); + } + + return deprecations; + }; +} diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 0d1ed4745d00..4c1fe784ea49 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -6,7 +6,6 @@ */ import { i18n } from '@kbn/i18n'; -import { SubFeaturePrivilegeGroupType } from '../../features/common'; import { LicenseType } from '../../licensing/common/types'; import { AlertType, APM_SERVER_FEATURE_ID } from '../common/alert_types'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; @@ -39,6 +38,9 @@ export const APM_FEATURE = { read: [], }, alerting: { + alert: { + all: Object.values(AlertType), + }, rule: { all: Object.values(AlertType), }, @@ -57,6 +59,9 @@ export const APM_FEATURE = { read: [], }, alerting: { + alert: { + read: Object.values(AlertType), + }, rule: { read: Object.values(AlertType), }, @@ -67,60 +72,6 @@ export const APM_FEATURE = { ui: ['show', 'alerting:show'], }, }, - subFeatures: [ - { - name: i18n.translate('xpack.apm.featureRegistry.manageAlertsName', { - defaultMessage: 'Alerts', - }), - privilegeGroups: [ - { - groupType: 'mutually_exclusive' as SubFeaturePrivilegeGroupType, - privileges: [ - { - id: 'alerts_all', - name: i18n.translate( - 'xpack.apm.featureRegistry.subfeature.alertsAllName', - { - defaultMessage: 'All', - } - ), - includeIn: 'all' as 'all', - alerting: { - alert: { - all: Object.values(AlertType), - }, - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - { - id: 'alerts_read', - name: i18n.translate( - 'xpack.apm.featureRegistry.subfeature.alertsReadName', - { - defaultMessage: 'Read', - } - ), - includeIn: 'read' as 'read', - alerting: { - alert: { - read: Object.values(AlertType), - }, - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - ], - }, - ], - }, - ], }; interface Feature { diff --git a/x-pack/plugins/apm/server/lib/event_metadata/get_event_metadata.ts b/x-pack/plugins/apm/server/lib/event_metadata/get_event_metadata.ts new file mode 100644 index 000000000000..97e2e1356363 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/event_metadata/get_event_metadata.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; +import { + ERROR_ID, + SPAN_ID, + TRANSACTION_ID, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import type { APMEventClient } from '../helpers/create_es_client/create_apm_event_client'; + +export async function getEventMetadata({ + apmEventClient, + processorEvent, + id, +}: { + apmEventClient: APMEventClient; + processorEvent: ProcessorEvent; + id: string; +}) { + const filter: QueryDslQueryContainer[] = []; + + switch (processorEvent) { + case ProcessorEvent.error: + filter.push({ + term: { [ERROR_ID]: id }, + }); + break; + + case ProcessorEvent.transaction: + filter.push({ + term: { + [TRANSACTION_ID]: id, + }, + }); + break; + + case ProcessorEvent.span: + filter.push({ + term: { [SPAN_ID]: id }, + }); + break; + } + + const response = await apmEventClient.search('get_event_metadata', { + apm: { + events: [processorEvent], + }, + body: { + query: { + bool: { filter }, + }, + size: 1, + _source: false, + fields: [{ field: '*', include_unmapped: true }], + }, + terminate_after: 1, + }); + + return response.hits.hits[0].fields; +} diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts index ebb5c7655806..9409e94fa9ba 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts @@ -5,6 +5,7 @@ * 2.0. */ +import moment from 'moment'; import { SetupUX } from '../../routes/rum_client'; import { SERVICE_NAME, @@ -16,8 +17,8 @@ import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types'; export async function hasRumData({ setup, - start, - end, + start = moment().subtract(24, 'h').valueOf(), + end = moment().valueOf(), }: { setup: SetupUX; start?: number; diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 56185d846562..2296227de2a3 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -51,6 +51,7 @@ import { TRANSACTION_TYPE, } from '../common/elasticsearch_fieldnames'; import { tutorialProvider } from './tutorial'; +import { getDeprecations } from './deprecations'; export class APMPlugin implements @@ -222,6 +223,12 @@ export class APMPlugin ); })(); }); + core.deprecations.registerDeprecations({ + getDeprecations: getDeprecations({ + cloudSetup: plugins.cloud, + fleet: resourcePlugins.fleet, + }), + }); return { config$: mergedConfig$, diff --git a/x-pack/plugins/apm/server/routes/event_metadata.ts b/x-pack/plugins/apm/server/routes/event_metadata.ts new file mode 100644 index 000000000000..8970ab8ffdee --- /dev/null +++ b/x-pack/plugins/apm/server/routes/event_metadata.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './create_apm_server_route'; +import { getEventMetadata } from '../lib/event_metadata/get_event_metadata'; +import { processorEventRt } from '../../common/processor_event'; +import { setupRequest } from '../lib/helpers/setup_request'; + +const eventMetadataRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + options: { tags: ['access:apm'] }, + params: t.type({ + path: t.type({ + processorEvent: processorEventRt, + id: t.string, + }), + }), + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { + path: { processorEvent, id }, + } = resources.params; + + const metadata = await getEventMetadata({ + apmEventClient: setup.apmEventClient, + processorEvent, + id, + }); + + return { + metadata, + }; + }, +}); + +export const eventMetadataRouteRepository = + createApmServerRouteRepository().add(eventMetadataRoute); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index 7aa520dd5b8a..472e46fecfa1 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -33,6 +33,7 @@ import { traceRouteRepository } from './traces'; import { transactionRouteRepository } from './transactions'; import { APMRouteHandlerResources } from './typings'; import { historicalDataRouteRepository } from './historical_data'; +import { eventMetadataRouteRepository } from './event_metadata'; import { suggestionsRouteRepository } from './suggestions'; const getTypedGlobalApmServerRouteRepository = () => { @@ -58,7 +59,8 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(apmFleetRouteRepository) .merge(backendsRouteRepository) .merge(fallbackToTransactionsRouteRepository) - .merge(historicalDataRouteRepository); + .merge(historicalDataRouteRepository) + .merge(eventMetadataRouteRepository); return repository; }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.ts index 4c37d9acc486..63c3f1dcabca 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { Datatable, ExpressionFunctionDefinition, getType } from '../../../types'; +import { + Datatable, + DatatableColumnType, + ExpressionFunctionDefinition, + getType, +} from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -30,14 +35,14 @@ export function asFn(): ExpressionFunctionDefinition<'as', Input, Arguments, Dat default: 'value', }, }, - fn: (input, args) => { + fn: (input, args): Datatable => { return { type: 'datatable', columns: [ { id: args.name, name: args.name, - meta: { type: getType(input) }, + meta: { type: getType(input) as DatatableColumnType }, }, ], rows: [ diff --git a/x-pack/plugins/canvas/public/components/function_reference_generator/generate_function_reference.ts b/x-pack/plugins/canvas/public/components/function_reference_generator/generate_function_reference.ts index 075e65bc24da..289704ae7953 100644 --- a/x-pack/plugins/canvas/public/components/function_reference_generator/generate_function_reference.ts +++ b/x-pack/plugins/canvas/public/components/function_reference_generator/generate_function_reference.ts @@ -156,7 +156,7 @@ ${examplesBlock} *Returns:* ${output ? wrapInBackTicks(output) : 'Depends on your input and arguments'}\n\n`; }; -const getArgsTable = (args: { [key: string]: ExpressionFunctionParameter }) => { +const getArgsTable = (args: { [key: string]: ExpressionFunctionParameter }) => { if (!args || Object.keys(args).length === 0) { return 'None'; } diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx index 45e8944c7c66..e2793512e23d 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx @@ -23,7 +23,7 @@ import { roundToDecimalPlace, kibanaFieldFormat } from '../utils'; import { ExpandedRowFieldHeader } from '../stats_table/components/expanded_row_field_header'; import { FieldVisStats } from '../../../../../common/types'; import { ExpandedRowPanel } from '../stats_table/components/field_data_expanded_row/expanded_row_panel'; -import { IndexPatternField } from '../../../../../../../../src/plugins/data/common/data_views/fields'; +import { IndexPatternField } from '../../../../../../../../src/plugins/data_views/common'; interface Props { stats: FieldVisStats | undefined; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts index 4fb50aecc106..3dd81015393b 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts @@ -13,7 +13,7 @@ import { import type { SavedSearchSavedObject } from '../../../../common'; import type { SavedSearch } from '../../../../../../../src/plugins/discover/public'; import type { Filter, FilterStateStore } from '@kbn/es-query'; -import { stubbedSavedObjectIndexPattern } from '../../../../../../../src/plugins/data/common/data_views/data_view.stub'; +import { stubbedSavedObjectIndexPattern } from '../../../../../../../src/plugins/data_views/common/data_view.stub'; import { IndexPattern } from '../../../../../../../src/plugins/data/common'; import { fieldFormatsMock } from '../../../../../../../src/plugins/field_formats/common/mocks'; import { uiSettingsServiceMock } from 'src/core/public/mocks'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx index 5afbce3661da..bf64101527fd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx @@ -16,7 +16,7 @@ import { nextTick } from '@kbn/test/jest'; import { DEFAULT_META } from '../../../../shared/constants'; -import { SuggestionsLogic } from './suggestions_logic'; +import { SuggestionsAPIResponse, SuggestionsLogic } from './suggestions_logic'; const DEFAULT_VALUES = { dataLoading: true, @@ -30,7 +30,7 @@ const DEFAULT_VALUES = { }, }; -const MOCK_RESPONSE = { +const MOCK_RESPONSE: SuggestionsAPIResponse = { meta: { page: { current: 1, @@ -44,6 +44,7 @@ const MOCK_RESPONSE = { query: 'foo', updated_at: '2021-07-08T14:35:50Z', promoted: ['1', '2'], + status: 'applied', }, ], }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx index 9352bdab51ed..074d2114ee8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx @@ -15,7 +15,7 @@ import { updateMetaPageIndex } from '../../../../shared/table_pagination'; import { EngineLogic } from '../../engine'; import { CurationSuggestion } from '../types'; -interface SuggestionsAPIResponse { +export interface SuggestionsAPIResponse { results: CurationSuggestion[]; meta: Meta; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts index f8c3e3efdbc1..01ca80776ae8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts @@ -50,6 +50,13 @@ export const RESTORE_CONFIRMATION = i18n.translate( } ); +export const CONVERT_TO_MANUAL_CONFIRMATION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.convertToManualCurationConfirmation', + { + defaultMessage: 'Are you sure you want to convert this to a manual curation?', + } +); + export const RESULT_ACTIONS_DIRECTIONS = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.resultActionsDescription', { defaultMessage: 'Promote results by clicking the star, hide them by clicking the eye.' } @@ -82,3 +89,13 @@ export const SHOW_DOCUMENT_ACTION = { iconType: 'eye', iconColor: 'primary' as EuiButtonIconColor, }; + +export const AUTOMATED_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curation.automatedLabel', + { defaultMessage: 'Automated' } +); + +export const COVERT_TO_MANUAL_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curation.convertToManualCurationButtonLabel', + { defaultMessage: 'Convert to manual curation' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx new file mode 100644 index 000000000000..3139d6286372 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic'; +import { mockUseParams } from '../../../../__mocks__/react_router'; +import '../../../__mocks__/engine_logic.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiBadge } from '@elastic/eui'; + +import { getPageHeaderActions, getPageTitle } from '../../../../test_helpers'; + +jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() })); + +import { AppSearchPageTemplate } from '../../layout'; + +import { AutomatedCuration } from './automated_curation'; +import { CurationLogic } from './curation_logic'; + +import { PromotedDocuments, OrganicDocuments } from './documents'; + +describe('AutomatedCuration', () => { + const values = { + dataLoading: false, + queries: ['query A', 'query B'], + isFlyoutOpen: false, + curation: { + suggestion: { + status: 'applied', + }, + }, + activeQuery: 'query A', + isAutomated: true, + }; + + const actions = { + convertToManual: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + mockUseParams.mockReturnValue({ curationId: 'test' }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.is(AppSearchPageTemplate)); + expect(wrapper.find(PromotedDocuments)).toHaveLength(1); + expect(wrapper.find(OrganicDocuments)).toHaveLength(1); + }); + + it('initializes CurationLogic with a curationId prop from URL param', () => { + mockUseParams.mockReturnValueOnce({ curationId: 'hello-world' }); + shallow(); + + expect(CurationLogic).toHaveBeenCalledWith({ curationId: 'hello-world' }); + }); + + it('displays the query in the title with a badge', () => { + const wrapper = shallow(); + const pageTitle = shallow(
{getPageTitle(wrapper)}
); + + expect(pageTitle.text()).toContain('query A'); + expect(pageTitle.find(EuiBadge)).toHaveLength(1); + }); + + describe('convert to manual button', () => { + let convertToManualButton: ShallowWrapper; + let confirmSpy: jest.SpyInstance; + + beforeAll(() => { + const wrapper = shallow(); + convertToManualButton = getPageHeaderActions(wrapper).childAt(0); + + confirmSpy = jest.spyOn(window, 'confirm'); + }); + + afterAll(() => { + confirmSpy.mockRestore(); + }); + + it('converts the curation upon user confirmation', () => { + confirmSpy.mockReturnValueOnce(true); + convertToManualButton.simulate('click'); + expect(actions.convertToManual).toHaveBeenCalled(); + }); + + it('does not convert the curation if the user cancels', () => { + confirmSpy.mockReturnValueOnce(false); + convertToManualButton.simulate('click'); + expect(actions.convertToManual).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx new file mode 100644 index 000000000000..1415537e42d6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { useValues, useActions } from 'kea'; + +import { EuiSpacer, EuiButton, EuiBadge } from '@elastic/eui'; + +import { AppSearchPageTemplate } from '../../layout'; +import { AutomatedIcon } from '../components/automated_icon'; +import { + AUTOMATED_LABEL, + COVERT_TO_MANUAL_BUTTON_LABEL, + CONVERT_TO_MANUAL_CONFIRMATION, +} from '../constants'; +import { getCurationsBreadcrumbs } from '../utils'; + +import { CurationLogic } from './curation_logic'; +import { PromotedDocuments, OrganicDocuments } from './documents'; + +export const AutomatedCuration: React.FC = () => { + const { curationId } = useParams<{ curationId: string }>(); + const logic = CurationLogic({ curationId }); + const { convertToManual } = useActions(logic); + const { activeQuery, dataLoading, queries } = useValues(logic); + + return ( + + {activeQuery}{' '} + + {AUTOMATED_LABEL} + + + ), + rightSideItems: [ + { + if (window.confirm(CONVERT_TO_MANUAL_CONFIRMATION)) convertToManual(); + }} + > + {COVERT_TO_MANUAL_BUTTON_LABEL} + , + ], + }} + isLoading={dataLoading} + > + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx index 2efe1f2ffe86..62c3a6c7d457 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx @@ -12,26 +12,25 @@ import '../../../__mocks__/engine_logic.mock'; import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; +import { shallow } from 'enzyme'; -import { rerender, getPageTitle, getPageHeaderActions } from '../../../../test_helpers'; +import { rerender } from '../../../../test_helpers'; jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() })); -import { CurationLogic } from './curation_logic'; -import { AddResultFlyout } from './results'; +import { AutomatedCuration } from './automated_curation'; + +import { ManualCuration } from './manual_curation'; import { Curation } from './'; describe('Curation', () => { const values = { - dataLoading: false, - queries: ['query A', 'query B'], - isFlyoutOpen: false, + isAutomated: true, }; + const actions = { loadCuration: jest.fn(), - resetCuration: jest.fn(), }; beforeEach(() => { @@ -40,32 +39,6 @@ describe('Curation', () => { setMockActions(actions); }); - it('renders', () => { - const wrapper = shallow(); - - expect(getPageTitle(wrapper)).toEqual('Manage curation'); - expect(wrapper.prop('pageChrome')).toEqual([ - 'Engines', - 'some-engine', - 'Curations', - 'query A, query B', - ]); - }); - - it('renders the add result flyout when open', () => { - setMockValues({ ...values, isFlyoutOpen: true }); - const wrapper = shallow(); - - expect(wrapper.find(AddResultFlyout)).toHaveLength(1); - }); - - it('initializes CurationLogic with a curationId prop from URL param', () => { - mockUseParams.mockReturnValueOnce({ curationId: 'hello-world' }); - shallow(); - - expect(CurationLogic).toHaveBeenCalledWith({ curationId: 'hello-world' }); - }); - it('calls loadCuration on page load & whenever the curationId URL param changes', () => { mockUseParams.mockReturnValueOnce({ curationId: 'cur-123456789' }); const wrapper = shallow(); @@ -76,31 +49,17 @@ describe('Curation', () => { expect(actions.loadCuration).toHaveBeenCalledTimes(2); }); - describe('restore defaults button', () => { - let restoreDefaultsButton: ShallowWrapper; - let confirmSpy: jest.SpyInstance; - - beforeAll(() => { - const wrapper = shallow(); - restoreDefaultsButton = getPageHeaderActions(wrapper).childAt(0); - - confirmSpy = jest.spyOn(window, 'confirm'); - }); + it('renders a view for automated curations', () => { + setMockValues({ isAutomated: true }); + const wrapper = shallow(); - afterAll(() => { - confirmSpy.mockRestore(); - }); + expect(wrapper.is(AutomatedCuration)).toBe(true); + }); - it('resets the curation upon user confirmation', () => { - confirmSpy.mockReturnValueOnce(true); - restoreDefaultsButton.simulate('click'); - expect(actions.resetCuration).toHaveBeenCalled(); - }); + it('renders a view for manual curations', () => { + setMockValues({ isAutomated: false }); + const wrapper = shallow(); - it('does not reset the curation if the user cancels', () => { - confirmSpy.mockReturnValueOnce(false); - restoreDefaultsButton.simulate('click'); - expect(actions.resetCuration).not.toHaveBeenCalled(); - }); + expect(wrapper.is(ManualCuration)).toBe(true); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx index 2a01c0db049a..19b6542e96c4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx @@ -10,64 +10,18 @@ import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; -import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; - -import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../../constants'; -import { AppSearchPageTemplate } from '../../layout'; -import { MANAGE_CURATION_TITLE, RESTORE_CONFIRMATION } from '../constants'; -import { getCurationsBreadcrumbs } from '../utils'; - +import { AutomatedCuration } from './automated_curation'; import { CurationLogic } from './curation_logic'; -import { PromotedDocuments, OrganicDocuments, HiddenDocuments } from './documents'; -import { ActiveQuerySelect, ManageQueriesModal } from './queries'; -import { AddResultLogic, AddResultFlyout } from './results'; +import { ManualCuration } from './manual_curation'; export const Curation: React.FC = () => { const { curationId } = useParams() as { curationId: string }; - const { loadCuration, resetCuration } = useActions(CurationLogic({ curationId })); - const { dataLoading, queries } = useValues(CurationLogic({ curationId })); - const { isFlyoutOpen } = useValues(AddResultLogic); + const { loadCuration } = useActions(CurationLogic({ curationId })); + const { isAutomated } = useValues(CurationLogic({ curationId })); useEffect(() => { loadCuration(); }, [curationId]); - return ( - { - if (window.confirm(RESTORE_CONFIRMATION)) resetCuration(); - }} - > - {RESTORE_DEFAULTS_BUTTON_LABEL} - , - ], - }} - isLoading={dataLoading} - > - - - - - - - - - - - - - - - - - - {isFlyoutOpen && } - - ); + return isAutomated ? : ; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts index 8fa57e52a26a..941fd0bf28f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts @@ -55,6 +55,7 @@ describe('CurationLogic', () => { promotedDocumentsLoading: false, hiddenIds: [], hiddenDocumentsLoading: false, + isAutomated: false, }; beforeEach(() => { @@ -265,7 +266,60 @@ describe('CurationLogic', () => { }); }); + describe('selectors', () => { + describe('isAutomated', () => { + it('is true when suggestion status is automated', () => { + mount({ curation: { suggestion: { status: 'automated' } } }); + + expect(CurationLogic.values.isAutomated).toBe(true); + }); + + it('is false when suggestion status is not automated', () => { + for (status of ['pending', 'applied', 'rejected', 'disabled']) { + mount({ curation: { suggestion: { status } } }); + + expect(CurationLogic.values.isAutomated).toBe(false); + } + }); + }); + }); + describe('listeners', () => { + describe('convertToManual', () => { + it('should make an API call and re-load the curation on success', async () => { + http.put.mockReturnValueOnce(Promise.resolve()); + mount({ activeQuery: 'some query' }); + jest.spyOn(CurationLogic.actions, 'loadCuration'); + + CurationLogic.actions.convertToManual(); + await nextTick(); + + expect(http.put).toHaveBeenCalledWith( + '/internal/app_search/engines/some-engine/search_relevance_suggestions', + { + body: JSON.stringify([ + { + query: 'some query', + type: 'curation', + status: 'applied', + }, + ]), + } + ); + expect(CurationLogic.actions.loadCuration).toHaveBeenCalled(); + }); + + it('flashes any error messages', async () => { + http.put.mockReturnValueOnce(Promise.reject('error')); + mount({ activeQuery: 'some query' }); + + CurationLogic.actions.convertToManual(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + describe('loadCuration', () => { it('should set dataLoading state', () => { mount({ dataLoading: false }, { curationId: 'cur-123456789' }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts index c49fc76d0687..a9fa5ab8c104 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts @@ -27,9 +27,11 @@ interface CurationValues { promotedDocumentsLoading: boolean; hiddenIds: string[]; hiddenDocumentsLoading: boolean; + isAutomated: boolean; } interface CurationActions { + convertToManual(): void; loadCuration(): void; onCurationLoad(curation: Curation): { curation: Curation }; updateCuration(): void; @@ -53,6 +55,7 @@ interface CurationProps { export const CurationLogic = kea>({ path: ['enterprise_search', 'app_search', 'curation_logic'], actions: () => ({ + convertToManual: true, loadCuration: true, onCurationLoad: (curation) => ({ curation }), updateCuration: true, @@ -162,7 +165,34 @@ export const CurationLogic = kea ({ + isAutomated: [ + () => [selectors.curation], + (curation: CurationValues['curation']) => { + return curation.suggestion?.status === 'automated'; + }, + ], + }), listeners: ({ actions, values, props }) => ({ + convertToManual: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + await http.put(`/internal/app_search/engines/${engineName}/search_relevance_suggestions`, { + body: JSON.stringify([ + { + query: values.activeQuery, + type: 'curation', + status: 'applied', + }, + ]), + }); + actions.loadCuration(); + } catch (e) { + flashAPIErrors(e); + } + }, loadCuration: async () => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx index 0624d0063e57..b7955cf51407 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx @@ -13,6 +13,8 @@ import { shallow } from 'enzyme'; import { EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui'; +import { mountWithIntl } from '../../../../../test_helpers'; + import { DataPanel } from '../../../data_panel'; import { CurationResult } from '../results'; @@ -30,6 +32,7 @@ describe('OrganicDocuments', () => { }, activeQuery: 'world', organicDocumentsLoading: false, + isAutomated: false, }; const actions = { addPromotedId: jest.fn(), @@ -56,6 +59,13 @@ describe('OrganicDocuments', () => { expect(titleText).toEqual('Top organic documents for "world"'); }); + it('shows a title when the curation is manual', () => { + setMockValues({ ...values, isAutomated: false }); + const wrapper = shallow(); + + expect(wrapper.find(DataPanel).prop('subtitle')).toContain('Promote results'); + }); + it('renders a loading state', () => { setMockValues({ ...values, organicDocumentsLoading: true }); const wrapper = shallow(); @@ -63,11 +73,21 @@ describe('OrganicDocuments', () => { expect(wrapper.find(EuiLoadingContent)).toHaveLength(1); }); - it('renders an empty state', () => { - setMockValues({ ...values, curation: { organic: [] } }); - const wrapper = shallow(); + describe('empty state', () => { + it('renders', () => { + setMockValues({ ...values, curation: { organic: [] } }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); - expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + it('tells the user to modify the query if the curation is manual', () => { + setMockValues({ ...values, curation: { organic: [] }, isAutomated: false }); + const wrapper = shallow(); + const emptyPromptBody = mountWithIntl(<>{wrapper.find(EuiEmptyPrompt).prop('body')}); + + expect(emptyPromptBody.text()).toContain('Add or change'); + }); }); describe('actions', () => { @@ -86,5 +106,13 @@ describe('OrganicDocuments', () => { expect(actions.addHiddenId).toHaveBeenCalledWith('mock-document-3'); }); + + it('hides actions when the curation is automated', () => { + setMockValues({ ...values, isAutomated: true }); + const wrapper = shallow(); + const result = wrapper.find(CurationResult).first(); + + expect(result.prop('actions')).toEqual([]); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx index a3a761feefcd..7314376a4a7a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx @@ -11,6 +11,7 @@ import { useValues, useActions } from 'kea'; import { EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { DataPanel } from '../../../data_panel'; import { Result } from '../../../result/types'; @@ -25,7 +26,7 @@ import { CurationResult } from '../results'; export const OrganicDocuments: React.FC = () => { const { addPromotedId, addHiddenId } = useActions(CurationLogic); - const { curation, activeQuery, organicDocumentsLoading } = useValues(CurationLogic); + const { curation, activeQuery, isAutomated, organicDocumentsLoading } = useValues(CurationLogic); const documents = curation.organic; const hasDocuments = documents.length > 0 && !organicDocumentsLoading; @@ -46,36 +47,50 @@ export const OrganicDocuments: React.FC = () => { )} } - subtitle={RESULT_ACTIONS_DIRECTIONS} + subtitle={!isAutomated && RESULT_ACTIONS_DIRECTIONS} > {hasDocuments ? ( documents.map((document: Result) => ( addHiddenId(document.id.raw), - }, - { - ...PROMOTE_DOCUMENT_ACTION, - onClick: () => addPromotedId(document.id.raw), - }, - ]} + actions={ + isAutomated + ? [] + : [ + { + ...HIDE_DOCUMENT_ACTION, + onClick: () => addHiddenId(document.id.raw), + }, + { + ...PROMOTE_DOCUMENT_ACTION, + onClick: () => addPromotedId(document.id.raw), + }, + ] + } /> )) ) : organicDocumentsLoading ? ( ) : ( + {' '} + + + ), + }} + /> + } /> )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.test.tsx index e0c6de973666..a66b33a47f35 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.test.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { setMockValues, setMockActions } from '../../../../../__mocks__/kea_logic'; import React from 'react'; @@ -13,6 +12,7 @@ import { shallow } from 'enzyme'; import { EuiDragDropContext, EuiDraggable, EuiEmptyPrompt, EuiButtonEmpty } from '@elastic/eui'; +import { mountWithIntl } from '../../../../../test_helpers'; import { DataPanel } from '../../../data_panel'; import { CurationResult } from '../results'; @@ -57,11 +57,50 @@ describe('PromotedDocuments', () => { }); }); - it('renders an empty state & hides the panel actions when empty', () => { + it('informs the user documents can be re-ordered if the curation is manual', () => { + setMockValues({ ...values, isAutomated: false }); + const wrapper = shallow(); + const subtitle = mountWithIntl(wrapper.prop('subtitle')); + + expect(subtitle.text()).toContain('Documents can be re-ordered'); + }); + + it('informs the user the curation is managed if the curation is automated', () => { + setMockValues({ ...values, isAutomated: true }); + const wrapper = shallow(); + const subtitle = mountWithIntl(wrapper.prop('subtitle')); + + expect(subtitle.text()).toContain('managed by App Search'); + }); + + describe('empty state', () => { + it('renders', () => { + setMockValues({ ...values, curation: { promoted: [] } }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + + it('hide information about starring documents if the curation is automated', () => { + setMockValues({ ...values, curation: { promoted: [] }, isAutomated: true }); + const wrapper = shallow(); + const emptyPromptBody = mountWithIntl(<>{wrapper.find(EuiEmptyPrompt).prop('body')}); + + expect(emptyPromptBody.text()).not.toContain('Star documents'); + }); + }); + + it('hides the panel actions when empty', () => { setMockValues({ ...values, curation: { promoted: [] } }); const wrapper = shallow(); - expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + expect(wrapper.find(DataPanel).prop('action')).toBe(false); + }); + + it('hides the panel actions when the curation is automated', () => { + setMockValues({ ...values, isAutomated: true }); + const wrapper = shallow(); + expect(wrapper.find(DataPanel).prop('action')).toBe(false); }); @@ -81,6 +120,14 @@ describe('PromotedDocuments', () => { expect(actions.removePromotedId).toHaveBeenCalledWith('mock-document-4'); }); + it('hides demote button for results when the curation is automated', () => { + setMockValues({ ...values, isAutomated: true }); + const wrapper = shallow(); + const result = getDraggableChildren(wrapper.find(EuiDraggable).last()); + + expect(result.prop('actions')).toEqual([]); + }); + it('renders a demote all button that demotes all hidden results', () => { const wrapper = shallow(); const panelActions = shallow(wrapper.find(DataPanel).prop('action') as React.ReactElement); @@ -89,7 +136,7 @@ describe('PromotedDocuments', () => { expect(actions.clearPromotedIds).toHaveBeenCalled(); }); - describe('draggging', () => { + describe('dragging', () => { it('calls setPromotedIds with the reordered list when users are done dragging', () => { const wrapper = shallow(); wrapper.find(EuiDragDropContext).simulate('dragEnd', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.tsx index 6b0a02aa2af5..e9d9136a45ac 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.tsx @@ -21,6 +21,7 @@ import { euiDragDropReorder, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { DataPanel } from '../../../data_panel'; @@ -29,7 +30,7 @@ import { CurationLogic } from '../curation_logic'; import { AddResultButton, CurationResult, convertToResultFormat } from '../results'; export const PromotedDocuments: React.FC = () => { - const { curation, promotedIds, promotedDocumentsLoading } = useValues(CurationLogic); + const { curation, isAutomated, promotedIds, promotedDocumentsLoading } = useValues(CurationLogic); const documents = curation.promoted; const hasDocuments = documents.length > 0; @@ -53,21 +54,33 @@ export const PromotedDocuments: React.FC = () => { )} } - subtitle={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.description', - { - defaultMessage: - 'Promoted results appear before organic results. Documents can be re-ordered.', - } - )} + subtitle={ + isAutomated ? ( + + ) : ( + + ) + } action={ + !isAutomated && hasDocuments && ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.removeAllButtonLabel', { defaultMessage: 'Demote all' } @@ -89,17 +102,22 @@ export const PromotedDocuments: React.FC = () => { draggableId={document.id} customDragHandle spacing="none" + isDragDisabled={isAutomated} > {(provided) => ( removePromotedId(document.id), - }, - ]} + actions={ + isAutomated + ? [] + : [ + { + ...DEMOTE_DOCUMENT_ACTION, + onClick: () => removePromotedId(document.id), + }, + ] + } dragHandleProps={provided.dragHandleProps} /> )} @@ -109,13 +127,22 @@ export const PromotedDocuments: React.FC = () => { ) : ( } /> )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx new file mode 100644 index 000000000000..ad9f3bcd64e3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic'; +import { mockUseParams } from '../../../../__mocks__/react_router'; +import '../../../__mocks__/engine_logic.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { getPageTitle, getPageHeaderActions } from '../../../../test_helpers'; + +jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() })); +import { CurationLogic } from './curation_logic'; + +import { ManualCuration } from './manual_curation'; +import { AddResultFlyout } from './results'; + +describe('ManualCuration', () => { + const values = { + dataLoading: false, + queries: ['query A', 'query B'], + isFlyoutOpen: false, + }; + const actions = { + resetCuration: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(getPageTitle(wrapper)).toEqual('Manage curation'); + expect(wrapper.prop('pageChrome')).toEqual([ + 'Engines', + 'some-engine', + 'Curations', + 'query A, query B', + ]); + }); + + it('renders the add result flyout when open', () => { + setMockValues({ ...values, isFlyoutOpen: true }); + const wrapper = shallow(); + + expect(wrapper.find(AddResultFlyout)).toHaveLength(1); + }); + + it('initializes CurationLogic with a curationId prop from URL param', () => { + mockUseParams.mockReturnValueOnce({ curationId: 'hello-world' }); + shallow(); + + expect(CurationLogic).toHaveBeenCalledWith({ curationId: 'hello-world' }); + }); + + describe('restore defaults button', () => { + let restoreDefaultsButton: ShallowWrapper; + let confirmSpy: jest.SpyInstance; + + beforeAll(() => { + const wrapper = shallow(); + restoreDefaultsButton = getPageHeaderActions(wrapper).childAt(0); + + confirmSpy = jest.spyOn(window, 'confirm'); + }); + + afterAll(() => { + confirmSpy.mockRestore(); + }); + + it('resets the curation upon user confirmation', () => { + confirmSpy.mockReturnValueOnce(true); + restoreDefaultsButton.simulate('click'); + expect(actions.resetCuration).toHaveBeenCalled(); + }); + + it('does not reset the curation if the user cancels', () => { + confirmSpy.mockReturnValueOnce(false); + restoreDefaultsButton.simulate('click'); + expect(actions.resetCuration).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx new file mode 100644 index 000000000000..d50575535bf2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { useValues, useActions } from 'kea'; + +import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; + +import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../../constants'; +import { AppSearchPageTemplate } from '../../layout'; +import { MANAGE_CURATION_TITLE, RESTORE_CONFIRMATION } from '../constants'; +import { getCurationsBreadcrumbs } from '../utils'; + +import { CurationLogic } from './curation_logic'; +import { PromotedDocuments, OrganicDocuments, HiddenDocuments } from './documents'; +import { ActiveQuerySelect, ManageQueriesModal } from './queries'; +import { AddResultLogic, AddResultFlyout } from './results'; + +export const ManualCuration: React.FC = () => { + const { curationId } = useParams() as { curationId: string }; + const { resetCuration } = useActions(CurationLogic({ curationId })); + const { dataLoading, queries } = useValues(CurationLogic({ curationId })); + const { isFlyoutOpen } = useValues(AddResultLogic); + + return ( + { + if (window.confirm(RESTORE_CONFIRMATION)) resetCuration(); + }} + > + {RESTORE_DEFAULTS_BUTTON_LABEL} + , + ], + }} + isLoading={dataLoading} + > + + + + + + + + + + + + + + + + + {isFlyoutOpen && } + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.test.tsx index 53cefdd00c67..5b5c814a24c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.test.tsx @@ -5,34 +5,43 @@ * 2.0. */ -import { setMockActions } from '../../../../../__mocks__/kea_logic'; +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; +import { shallow } from 'enzyme'; import { EuiButton } from '@elastic/eui'; import { AddResultButton } from './'; describe('AddResultButton', () => { + const values = { + isAutomated: false, + }; + const actions = { openFlyout: jest.fn(), }; - let wrapper: ShallowWrapper; - - beforeAll(() => { - setMockActions(actions); - wrapper = shallow(); - }); - it('renders', () => { - expect(wrapper.find(EuiButton)).toHaveLength(1); + const wrapper = shallow(); + + expect(wrapper.is(EuiButton)).toBe(true); }); it('opens the add result flyout on click', () => { + setMockActions(actions); + const wrapper = shallow(); + wrapper.find(EuiButton).simulate('click'); expect(actions.openFlyout).toHaveBeenCalled(); }); + + it('is disbled when the curation is automated', () => { + setMockValues({ ...values, isAutomated: true }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('disabled')).toBe(true); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.tsx index 025dda65f4fb..f2285064da30 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.tsx @@ -7,18 +7,21 @@ import React from 'react'; -import { useActions } from 'kea'; +import { useActions, useValues } from 'kea'; import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { CurationLogic } from '..'; + import { AddResultLogic } from './'; export const AddResultButton: React.FC = () => { const { openFlyout } = useActions(AddResultLogic); + const { isAutomated } = useValues(CurationLogic); return ( - + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.addResult.buttonLabel', { defaultMessage: 'Add result manually', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts index 866bf6490ebe..09c8a487b1b9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts @@ -12,7 +12,9 @@ export interface CurationSuggestion { query: string; updated_at: string; promoted: string[]; + status: 'pending' | 'applied' | 'automated' | 'rejected' | 'disabled'; } + export interface Curation { id: string; last_updated: string; @@ -20,6 +22,7 @@ export interface Curation { promoted: CurationResult[]; hidden: CurationResult[]; organic: Result[]; + suggestion?: CurationSuggestion; } export interface CurationsAPIResponse { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.test.tsx index fad4e54721bb..80d5a874c810 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import { setMockValues } from '../../../../../__mocks__/kea_logic'; + import React from 'react'; import { shallow } from 'enzyme'; @@ -14,6 +16,13 @@ import { Result } from '../../../result'; import { CurationResultPanel } from './curation_result_panel'; describe('CurationResultPanel', () => { + const values = { + isMetaEngine: true, + engine: { + schema: {}, + }, + }; + const results = [ { id: { raw: 'foo' }, @@ -25,6 +34,10 @@ describe('CurationResultPanel', () => { }, ]; + beforeAll(() => { + setMockValues(values); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -33,6 +46,11 @@ describe('CurationResultPanel', () => { const wrapper = shallow(); expect(wrapper.find('[data-test-subj="suggestedText"]').exists()).toBe(false); expect(wrapper.find(Result).length).toBe(2); + expect(wrapper.find(Result).at(0).props()).toEqual({ + result: results[0], + isMetaEngine: true, + schemaForTypeHighlights: values.engine.schema, + }); }); it('renders a no results message when there are no results', () => { @@ -41,6 +59,11 @@ describe('CurationResultPanel', () => { expect(wrapper.find(Result).length).toBe(0); }); + it('renders the correct count', () => { + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="curationCount"]').prop('children')).toBe(2); + }); + it('shows text about automation when variant is "suggested"', () => { const wrapper = shallow(); expect(wrapper.find('[data-test-subj="suggestedText"]').exists()).toBe(true); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.tsx index 12bbf07f97bb..b61355d0b855 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.tsx @@ -7,6 +7,8 @@ import React from 'react'; +import { useValues } from 'kea'; + import { EuiFlexGroup, EuiFlexItem, @@ -17,6 +19,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { EngineLogic } from '../../../engine'; + import { Result } from '../../../result'; import { Result as ResultType } from '../../../result/types'; import './curation_result_panel.scss'; @@ -27,14 +31,14 @@ interface Props { } export const CurationResultPanel: React.FC = ({ variant, results }) => { - // TODO wire up - const count = 3; + const { isMetaEngine, engine } = useValues(EngineLogic); + const count = results.length; return ( <> - {count} + {count} @@ -70,7 +74,11 @@ export const CurationResultPanel: React.FC = ({ variant, results }) => { {results.length > 0 ? ( results.map((result) => ( - + )) ) : ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.test.tsx index 9bfc12dfe7cc..2dcefa7273c7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; +import '../../../../../__mocks__/shallow_useeffect.mock'; import { mockUseParams } from '../../../../../__mocks__/react_router'; import '../../../../__mocks__/engine_logic.mock'; @@ -14,12 +16,65 @@ import { shallow } from 'enzyme'; import { AppSearchPageTemplate } from '../../../layout'; +import { Result } from '../../../result'; + +import { CurationResultPanel } from './curation_result_panel'; import { CurationSuggestion } from './curation_suggestion'; describe('CurationSuggestion', () => { + const values = { + suggestion: { + query: 'foo', + updated_at: '2021-07-08T14:35:50Z', + promoted: ['1', '2', '3'], + }, + suggestedPromotedDocuments: [ + { + id: { + raw: '1', + }, + _meta: { + id: '1', + engine: 'some-engine', + }, + }, + { + id: { + raw: '2', + }, + _meta: { + id: '2', + engine: 'some-engine', + }, + }, + { + id: { + raw: '3', + }, + _meta: { + id: '3', + engine: 'some-engine', + }, + }, + ], + isMetaEngine: true, + engine: { + schema: {}, + }, + }; + + const actions = { + loadSuggestion: jest.fn(), + }; + + beforeAll(() => { + setMockValues(values); + setMockActions(actions); + }); + beforeEach(() => { jest.clearAllMocks(); - mockUseParams.mockReturnValue({ query: 'some%20query' }); + mockUseParams.mockReturnValue({ query: 'foo' }); }); it('renders', () => { @@ -28,19 +83,21 @@ describe('CurationSuggestion', () => { expect(wrapper.is(AppSearchPageTemplate)).toBe(true); }); - it('displays the decoded query in the title', () => { - const wrapper = shallow(); - - expect(wrapper.prop('pageHeader').pageTitle).toEqual('some query'); + it('loads data on initialization', () => { + shallow(); + expect(actions.loadSuggestion).toHaveBeenCalled(); }); - // TODO This will need to come from somewhere else when wired up - it('displays an empty query if "" is encoded in as the qery', () => { - mockUseParams.mockReturnValue({ query: '%22%22' }); + it('shows suggested promoted documents', () => { + const wrapper = shallow(); + const suggestedResultsPanel = wrapper.find(CurationResultPanel).at(1); + expect(suggestedResultsPanel.prop('results')).toEqual(values.suggestedPromotedDocuments); + }); + it('displays the query in the title', () => { const wrapper = shallow(); - expect(wrapper.prop('pageHeader').pageTitle).toEqual('""'); + expect(wrapper.prop('pageHeader').pageTitle).toEqual('foo'); }); it('displays has a button to display organic results', () => { @@ -52,4 +109,24 @@ describe('CurationSuggestion', () => { wrapper.find('[data-test-subj="showOrganicResults"]').simulate('click'); expect(wrapper.find('[data-test-subj="organicResults"]').exists()).toBe(false); }); + + it('displays proposed organic results', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="showOrganicResults"]').simulate('click'); + expect(wrapper.find('[data-test-subj="proposedOrganicResults"]').find(Result).length).toBe(4); + expect(wrapper.find(Result).at(0).prop('isMetaEngine')).toEqual(true); + expect(wrapper.find(Result).at(0).prop('schemaForTypeHighlights')).toEqual( + values.engine.schema + ); + }); + + it('displays current organic results', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="showOrganicResults"]').simulate('click'); + expect(wrapper.find('[data-test-subj="currentOrganicResults"]').find(Result).length).toBe(4); + expect(wrapper.find(Result).at(0).prop('isMetaEngine')).toEqual(true); + expect(wrapper.find(Result).at(0).prop('schemaForTypeHighlights')).toEqual( + values.engine.schema + ); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.tsx index 4fab9db47af9..ade78e4914e8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; + +import { useActions, useValues } from 'kea'; import { EuiButtonEmpty, @@ -19,6 +21,7 @@ import { import { i18n } from '@kbn/i18n'; import { useDecodedParams } from '../../../../utils/encode_path_params'; +import { EngineLogic } from '../../../engine'; import { AppSearchPageTemplate } from '../../../layout'; import { Result } from '../../../result'; import { Result as ResultType } from '../../../result/types'; @@ -27,26 +30,37 @@ import { getCurationsBreadcrumbs } from '../../utils'; import { CurationActionBar } from './curation_action_bar'; import { CurationResultPanel } from './curation_result_panel'; +import { CurationSuggestionLogic } from './curation_suggestion_logic'; import { DATA } from './temp_data'; export const CurationSuggestion: React.FC = () => { const { query } = useDecodedParams(); + const curationSuggestionLogic = CurationSuggestionLogic({ query }); + const { loadSuggestion } = useActions(curationSuggestionLogic); + const { engine, isMetaEngine } = useValues(EngineLogic); + const { suggestion, suggestedPromotedDocuments, dataLoading } = + useValues(curationSuggestionLogic); const [showOrganicResults, setShowOrganicResults] = useState(false); const currentOrganicResults = [...DATA].splice(5, 4); const proposedOrganicResults = [...DATA].splice(2, 4); - const queryTitle = query === '""' ? query : `${query}`; + const suggestionQuery = suggestion?.query || ''; + + useEffect(() => { + loadSuggestion(); + }, []); return ( { -

Current

+

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.currentTitle', + { defaultMessage: 'Current' } + )} +

-

Suggested

+

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.suggestionTitle', + { defaultMessage: 'Suggested' } + )} +

- +
@@ -81,7 +105,15 @@ export const CurationSuggestion: React.FC = () => { onClick={() => setShowOrganicResults(!showOrganicResults)} data-test-subj="showOrganicResults" > - {showOrganicResults ? 'Collapse' : 'Expand'} organic search results + {showOrganicResults + ? i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.collapseButtonLabel', + { defaultMessage: 'Collapse organic search results' } + ) + : i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.expandButtonLabel', + { defaultMessage: 'Expand organic search results' } + )}
{showOrganicResults && ( <> @@ -90,10 +122,18 @@ export const CurationSuggestion: React.FC = () => { {currentOrganicResults.length > 0 && ( - + {currentOrganicResults.map((result: ResultType) => ( - + ))} @@ -101,10 +141,18 @@ export const CurationSuggestion: React.FC = () => { {proposedOrganicResults.length > 0 && ( - + {proposedOrganicResults.map((result: ResultType) => ( - + ))} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts new file mode 100644 index 000000000000..9edeab4b658e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + LogicMounter, + mockFlashMessageHelpers, + mockHttpValues, +} from '../../../../../__mocks__/kea_logic'; + +import '../../../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { CurationSuggestion } from '../../types'; + +import { CurationSuggestionLogic } from './curation_suggestion_logic'; + +const DEFAULT_VALUES = { + dataLoading: true, + suggestion: null, + suggestedPromotedDocuments: [], +}; + +const suggestion: CurationSuggestion = { + query: 'foo', + updated_at: '2021-07-08T14:35:50Z', + promoted: ['1', '2', '3'], + status: 'applied', +}; + +const suggestedPromotedDocuments = [ + { + id: { + raw: '1', + }, + _meta: { + id: '1', + engine: 'some-engine', + }, + }, + { + id: { + raw: '2', + }, + _meta: { + id: '2', + engine: 'some-engine', + }, + }, + { + id: { + raw: '3', + }, + _meta: { + id: '3', + engine: 'some-engine', + }, + }, +]; + +const MOCK_RESPONSE = { + meta: { + page: { + current: 1, + size: 10, + total_results: 1, + total_pages: 1, + }, + }, + results: [suggestion], +}; + +const MOCK_DOCUMENTS_RESPONSE = { + results: [ + { + id: { + raw: '2', + }, + _meta: { + id: '2', + engine: 'some-engine', + }, + }, + { + id: { + raw: '1', + }, + _meta: { + id: '1', + engine: 'some-engine', + }, + }, + ], +}; + +describe('CurationSuggestionLogic', () => { + const { mount } = new LogicMounter(CurationSuggestionLogic); + const { flashAPIErrors } = mockFlashMessageHelpers; + const mountLogic = (props: object = {}) => { + mount(props, { query: 'foo-query' }); + }; + + const { http } = mockHttpValues; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mountLogic(); + expect(CurationSuggestionLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onSuggestionLoaded', () => { + it('should save the loaded suggestion and promoted documents associated with that suggestion and set dataLoading to false', () => { + mountLogic(); + CurationSuggestionLogic.actions.onSuggestionLoaded({ + suggestion, + suggestedPromotedDocuments, + }); + expect(CurationSuggestionLogic.values).toEqual({ + ...DEFAULT_VALUES, + suggestion, + suggestedPromotedDocuments, + dataLoading: false, + }); + }); + }); + }); + + describe('listeners', () => { + describe('loadSuggestion', () => { + it('should set dataLoading state', () => { + mountLogic({ dataLoading: false }); + + CurationSuggestionLogic.actions.loadSuggestion(); + + expect(CurationSuggestionLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + }); + }); + + it('should make an API call and trigger onSuggestionLoaded', async () => { + http.post.mockReturnValueOnce(Promise.resolve(MOCK_RESPONSE)); + http.post.mockReturnValueOnce(Promise.resolve(MOCK_DOCUMENTS_RESPONSE)); + mountLogic(); + jest.spyOn(CurationSuggestionLogic.actions, 'onSuggestionLoaded'); + + CurationSuggestionLogic.actions.loadSuggestion(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/internal/app_search/engines/some-engine/search_relevance_suggestions/foo-query', + { + body: JSON.stringify({ + page: { + current: 1, + size: 1, + }, + filters: { + status: ['pending'], + type: 'curation', + }, + }), + } + ); + + expect(http.post).toHaveBeenCalledWith('/internal/app_search/engines/some-engine/search', { + query: { query: '' }, + body: JSON.stringify({ + page: { + size: 100, + }, + filters: { + // The results of the first API call are used to make the second http call for document details + id: MOCK_RESPONSE.results[0].promoted, + }, + }), + }); + + expect(CurationSuggestionLogic.actions.onSuggestionLoaded).toHaveBeenCalledWith({ + suggestion: { + query: 'foo', + updated_at: '2021-07-08T14:35:50Z', + promoted: ['1', '2', '3'], + status: 'applied', + }, + // Note that these were re-ordered to match the 'promoted' list above, and since document + // 3 was not found it is not included in this list + suggestedPromotedDocuments: [ + { + id: { + raw: '1', + }, + _meta: { + id: '1', + engine: 'some-engine', + }, + }, + { + id: { + raw: '2', + }, + _meta: { + id: '2', + engine: 'some-engine', + }, + }, + ], + }); + }); + + it('handles errors', async () => { + http.post.mockReturnValueOnce(Promise.reject('error')); + mount(); + + CurationSuggestionLogic.actions.loadSuggestion(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.ts new file mode 100644 index 000000000000..d3f27be12206 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { flashAPIErrors } from '../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../shared/http'; +import { EngineLogic } from '../../../engine'; +import { Result } from '../../../result/types'; +import { CurationSuggestion } from '../../types'; + +interface CurationSuggestionValues { + dataLoading: boolean; + suggestion: CurationSuggestion | null; + suggestedPromotedDocuments: Result[]; +} + +interface CurationSuggestionActions { + loadSuggestion(): void; + onSuggestionLoaded({ + suggestion, + suggestedPromotedDocuments, + }: { + suggestion: CurationSuggestion; + suggestedPromotedDocuments: Result[]; + }): { + suggestion: CurationSuggestion; + suggestedPromotedDocuments: Result[]; + }; +} + +interface CurationSuggestionProps { + query: CurationSuggestion['query']; +} + +export const CurationSuggestionLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'curations', 'suggestion_logic'], + actions: () => ({ + loadSuggestion: true, + onSuggestionLoaded: ({ suggestion, suggestedPromotedDocuments }) => ({ + suggestion, + suggestedPromotedDocuments, + }), + }), + reducers: () => ({ + dataLoading: [ + true, + { + loadSuggestion: () => true, + onSuggestionLoaded: () => false, + }, + ], + suggestion: [ + null, + { + onSuggestionLoaded: (_, { suggestion }) => suggestion, + }, + ], + suggestedPromotedDocuments: [ + [], + { + onSuggestionLoaded: (_, { suggestedPromotedDocuments }) => suggestedPromotedDocuments, + }, + ], + }), + listeners: ({ actions, props }) => ({ + loadSuggestion: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response = await http.post( + `/internal/app_search/engines/${engineName}/search_relevance_suggestions/${props.query}`, + { + body: JSON.stringify({ + page: { + current: 1, + size: 1, + }, + filters: { + status: ['pending'], + type: 'curation', + }, + }), + } + ); + + const suggestion = response.results[0]; + + const searchResponse = await http.post( + `/internal/app_search/engines/${engineName}/search`, + { + query: { query: '' }, + body: JSON.stringify({ + page: { + size: 100, + }, + filters: { + id: suggestion.promoted, + }, + }), + } + ); + + // Filter out docs that were not found and maintain promoted order + const promotedIds: string[] = suggestion.promoted; + const documentDetails = searchResponse.results; + const suggestedPromotedDocuments = promotedIds.reduce((acc: Result[], id: string) => { + const found = documentDetails.find( + (documentDetail: Result) => documentDetail.id.raw === id + ); + if (!found) return acc; + return [...acc, found]; + }, []); + + actions.onSuggestionLoaded({ + suggestion: suggestion as CurationSuggestion, + suggestedPromotedDocuments, + }); + } catch (e) { + flashAPIErrors(e); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx index 52fbee90fe31..5eac38b88937 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx @@ -18,7 +18,7 @@ interface Props { export const ResultActions: React.FC = ({ actions }) => { return ( - {actions.map(({ onClick, title, iconType, iconColor }) => ( + {actions.map(({ onClick, title, iconType, iconColor, disabled }) => ( = ({ actions }) => { color={iconColor ? iconColor : 'primary'} aria-label={title} title={title} + disabled={disabled} /> ))} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts index 4be3eb137177..d9f1bb394778 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts @@ -41,4 +41,5 @@ export interface ResultAction { title: string; iconType: string; iconColor?: EuiButtonIconColor; + disabled?: boolean; } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts index 2504f51626bb..6075f6e9822d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts @@ -8,4 +8,5 @@ export * from './actions'; export * from './labels'; export * from './tables'; +export * from './units'; export { DEFAULT_META } from './default_meta'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/units.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/units.ts index 9d8c30c7e57d..fdf879807bc6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/units.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/units.ts @@ -7,6 +7,10 @@ import { i18n } from '@kbn/i18n'; +export const MINUTES_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.minutesLabel', { + defaultMessage: 'Minutes', +}); + export const HOURS_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.hoursLabel', { defaultMessage: 'Hours', }); @@ -22,3 +26,27 @@ export const WEEKS_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.wee export const MONTHS_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.monthsLabel', { defaultMessage: 'Months', }); + +export const DAYS_OF_WEEK_LABELS = { + SUNDAY: i18n.translate('xpack.enterpriseSearch.units.daysOfWeekLabel.sunday', { + defaultMessage: 'Sunday', + }), + MONDAY: i18n.translate('xpack.enterpriseSearch.units.daysOfWeekLabel.monday', { + defaultMessage: 'Monday', + }), + TUESDAY: i18n.translate('xpack.enterpriseSearch.units.daysOfWeekLabel.tuesday', { + defaultMessage: 'Tuesday', + }), + WEDNESDAY: i18n.translate('xpack.enterpriseSearch.units.daysOfWeekLabel.wednesday', { + defaultMessage: 'Wednesday', + }), + THURSDAY: i18n.translate('xpack.enterpriseSearch.units.daysOfWeekLabel.thursday', { + defaultMessage: 'Thursday', + }), + FRIDAY: i18n.translate('xpack.enterpriseSearch.units.daysOfWeekLabel.friday', { + defaultMessage: 'Friday', + }), + SATURDAY: i18n.translate('xpack.enterpriseSearch.units.daysOfWeekLabel.saturday', { + defaultMessage: 'Saturday', + }), +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 28e796c25639..e7b4e543b5eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -50,8 +50,28 @@ const defaultIndexing = { defaultAction: 'include', rules: [], schedule: { - intervals: [], - blocked: [], + full: 'P1D', + incremental: 'P2H', + delete: 'P10M', + permissions: 'P3H', + estimates: { + full: { + nextStart: '2021-09-30T15:37:38+00:00', + duration: 'PT1M5S', + }, + incremental: { + nextStart: '2021-09-27T17:39:24+00:00', + duration: 'PT2S', + }, + delete: { + nextStart: '2021-09-27T21:39:24+00:00', + duration: 'PT49S', + }, + permissions: { + nextStart: '2021-09-27T17:39:24+00:00', + duration: 'PT2S', + }, + }, }, features: { contentExtraction: { @@ -90,6 +110,7 @@ export const fullContentSources = [ groups, custom: false, isIndexedSource: true, + isSyncConfigEnabled: true, areThumbnailsConfigEnabled: true, accessToken: '123token', urlField: 'myLink', @@ -111,6 +132,7 @@ export const fullContentSources = [ indexing: defaultIndexing, custom: true, isIndexedSource: true, + isSyncConfigEnabled: true, areThumbnailsConfigEnabled: true, accessToken: '123token', urlField: 'url', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 3666a794617a..4a3b6a11c707 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -48,6 +48,21 @@ export const NAV = { SCHEMA: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.schema', { defaultMessage: 'Schema', }), + SYNCHRONIZATION: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.synchronization', { + defaultMessage: 'Synchronization', + }), + SYNCHRONIZATION_FREQUENCY: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.nav.synchronizationFrequency', + { + defaultMessage: 'Frequency', + } + ), + SYNCHRONIZATION_OBJECTS_AND_ASSETS: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.nav.synchronizationObjectsAndAssets', + { + defaultMessage: 'Objects and assets', + } + ), DISPLAY_SETTINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.displaySettings', { defaultMessage: 'Display Settings', }), @@ -745,6 +760,18 @@ export const DESCRIPTION_LABEL = i18n.translate( } ); +export const BLOCK_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.blockLabel', { + defaultMessage: 'Block', +}); + +export const BETWEEN_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.betweenLabel', { + defaultMessage: 'between', +}); + +export const EVERY_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.everyLabel', { + defaultMessage: 'every', +}); + export const AND = i18n.translate('xpack.enterpriseSearch.workplaceSearch.and', { defaultMessage: 'and', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index cd699f7df86c..691b52c9f51e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -45,6 +45,10 @@ export const CUSTOM_SOURCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-ap export const CUSTOM_API_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-sources-api.html`; export const CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL = `${CUSTOM_SOURCE_DOCS_URL}#custom-api-source-document-level-access-control`; export const ENT_SEARCH_LICENSE_MANAGEMENT = `${docLinks.enterpriseSearchBase}/license-management.html`; +export const SYNCHRONIZATION_DOCS_URL = '#TODO'; +export const DIFFERENT_SYNC_TYPES_DOCS_URL = '#TODO'; +export const SYNC_BEST_PRACTICES_DOCS_URL = '#TODO'; +export const OBJECTS_AND_ASSETS_DOCS_URL = '#TODO'; export const PERSONAL_PATH = '/p'; @@ -89,12 +93,17 @@ export const SOURCE_DETAILS_PATH = `${SOURCES_PATH}/:sourceId`; export const SOURCE_CONTENT_PATH = `${SOURCES_PATH}/:sourceId/content`; export const SOURCE_SCHEMAS_PATH = `${SOURCES_PATH}/:sourceId/schemas`; export const SOURCE_DISPLAY_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/display_settings`; +export const SOURCE_SYNCHRONIZATION_PATH = `${SOURCES_PATH}/:sourceId/synchronization`; export const SOURCE_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/settings`; export const REINDEX_JOB_PATH = `${SOURCE_SCHEMAS_PATH}/:activeReindexJobId`; export const DISPLAY_SETTINGS_SEARCH_RESULT_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/`; export const DISPLAY_SETTINGS_RESULT_DETAIL_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/result_detail`; +export const SYNC_FREQUENCY_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/frequency`; +export const BLOCKED_TIME_WINDOWS_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/frequency/blocked_windows`; +export const OBJECTS_AND_ASSETS_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/objects_and_assets`; + export const ORG_SETTINGS_PATH = '/settings'; export const ORG_SETTINGS_CUSTOMIZE_PATH = `${ORG_SETTINGS_PATH}/customize`; export const ORG_SETTINGS_CONNECTORS_PATH = `${ORG_SETTINGS_PATH}/connectors`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index c524bd4f7617..9956eae229a0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { Moment } from 'moment'; + import { RoleMapping } from '../shared/types'; export * from '../../../common/types/workplace_search'; @@ -128,7 +130,44 @@ interface SourceActivity { status: string; } -interface IndexingConfig { +export interface SyncEstimate { + duration?: string; + nextStart: string; + lastRun?: string; +} + +interface SyncIndexItem { + full: T; + incremental: T; + delete: T; + permissions?: T; +} + +interface IndexingSchedule extends SyncIndexItem { + estimates: SyncIndexItem; +} + +export type SyncJobType = 'full' | 'incremental' | 'delete' | 'permissions'; + +export const DAYS_OF_WEEK_VALUES = [ + 'sunday', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', +] as const; +export type DayOfWeek = typeof DAYS_OF_WEEK_VALUES[number]; + +export interface BlockedWindow { + jobType: SyncJobType; + day: DayOfWeek; + start: Moment; + end: Moment; +} + +export interface IndexingConfig { enabled: boolean; features: { contentExtraction: { @@ -138,6 +177,7 @@ interface IndexingConfig { enabled: boolean; }; }; + schedule: IndexingSchedule; } export interface ContentSourceFullData extends ContentSourceDetails { @@ -148,6 +188,7 @@ export interface ContentSourceFullData extends ContentSourceDetails { indexing: IndexingConfig; custom: boolean; isIndexedSource: boolean; + isSyncConfigEnabled: boolean; areThumbnailsConfigEnabled: boolean; accessToken: string; urlField: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx index 7f07c59587f9..d5ba030e582b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx @@ -7,6 +7,8 @@ import { setMockValues } from '../../../../__mocks__/kea_logic'; +import React from 'react'; + jest.mock('../../../../shared/layout', () => ({ generateNavLink: jest.fn(({ to }) => ({ href: to })), })); @@ -21,9 +23,13 @@ describe('useSourceSubNav', () => { }); it('returns EUI nav items', () => { - setMockValues({ isOrganization: true, contentSource: { id: '1' } }); + setMockValues({ isOrganization: true, contentSource: { id: '1', name: 'foo' } }); expect(useSourceSubNav()).toEqual([ + { + id: 'sourceName', + name: foo, + }, { id: 'sourceOverview', name: 'Overview', @@ -43,9 +49,16 @@ describe('useSourceSubNav', () => { }); it('returns extra nav items for custom sources', () => { - setMockValues({ isOrganization: true, contentSource: { id: '2', serviceType: 'custom' } }); + setMockValues({ + isOrganization: true, + contentSource: { id: '2', serviceType: 'custom', name: 'foo' }, + }); expect(useSourceSubNav()).toEqual([ + { + id: 'sourceName', + name: foo, + }, { id: 'sourceOverview', name: 'Overview', @@ -74,10 +87,60 @@ describe('useSourceSubNav', () => { ]); }); + it('returns extra nav items for synchronization', () => { + setMockValues({ + isOrganization: true, + contentSource: { id: '2', isIndexedSource: true, name: 'foo', isSyncConfigEnabled: true }, + }); + + expect(useSourceSubNav()).toEqual([ + { + id: 'sourceName', + name: foo, + }, + { + id: 'sourceOverview', + name: 'Overview', + href: '/sources/2', + }, + { + id: 'sourceContent', + name: 'Content', + href: '/sources/2/content', + }, + { + id: 'sourceSynchronization', + name: 'Synchronization', + href: '/sources/2/synchronization', + items: [ + { + id: 'sourceSynchronizationFrequency', + name: 'Frequency', + href: '/sources/2/synchronization/frequency', + }, + { + id: 'sourceSynchronizationObjectsAndAssets', + name: 'Objects and assets', + href: '/sources/2/synchronization/objects_and_assets', + }, + ], + }, + { + id: 'sourceSettings', + name: 'Settings', + href: '/sources/2/settings', + }, + ]); + }); + it('returns nav links to personal dashboard when not on an organization page', () => { - setMockValues({ isOrganization: false, contentSource: { id: '3' } }); + setMockValues({ isOrganization: false, contentSource: { id: '3', name: 'foo' } }); expect(useSourceSubNav()).toEqual([ + { + id: 'sourceName', + name: foo, + }, { id: 'sourceOverview', name: 'Overview', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx index 6b595a06f040..cae1e8834cdd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import React from 'react'; + import { useValues } from 'kea'; import { EuiSideNavItemType } from '@elastic/eui'; @@ -19,18 +21,29 @@ import { SOURCE_SCHEMAS_PATH, SOURCE_DISPLAY_SETTINGS_PATH, SOURCE_SETTINGS_PATH, + SOURCE_SYNCHRONIZATION_PATH, } from '../../../routes'; import { SourceLogic } from '../source_logic'; +import { useSynchronizationSubNav } from './synchronization/synchronization_sub_nav'; + export const useSourceSubNav = () => { const { isOrganization } = useValues(AppLogic); + const syncSubnav = useSynchronizationSubNav(); const { - contentSource: { id, serviceType }, + contentSource: { id, serviceType, isIndexedSource, name }, } = useValues(SourceLogic); if (!id) return undefined; + const isCustom = serviceType === CUSTOM_SERVICE_TYPE; + const showSynchronization = isIndexedSource && isOrganization; + const navItems: Array> = [ + { + id: 'sourceName', + name: {name}, + }, { id: 'sourceOverview', name: NAV.OVERVIEW, @@ -43,7 +56,17 @@ export const useSourceSubNav = () => { }, ]; - const isCustom = serviceType === CUSTOM_SERVICE_TYPE; + if (showSynchronization) { + navItems.push({ + id: 'sourceSynchronization', + name: NAV.SYNCHRONIZATION, + ...generateNavLink({ + to: getContentSourcePath(SOURCE_SYNCHRONIZATION_PATH, id, isOrganization), + }), + items: syncSubnav, + }); + } + if (isCustom) { navItems.push({ id: 'sourceSchema', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/__mocks__/syncronization.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/__mocks__/syncronization.mock.ts new file mode 100644 index 000000000000..a71be55339f4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/__mocks__/syncronization.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; + +import { SyncJobType, DayOfWeek } from '../../../../../types'; + +export const blockedWindow = { + jobType: 'incremental' as SyncJobType, + day: 'sunday' as DayOfWeek, + start: moment().set('hour', 11).set('minutes', 0), + end: moment().set('hour', 13).set('minutes', 0), +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.test.tsx new file mode 100644 index 000000000000..9f8f0b08f3ca --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { blockedWindow } from './__mocks__/syncronization.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiComboBox, EuiDatePicker, EuiSuperSelect } from '@elastic/eui'; + +import { BlockedWindowItem } from './blocked_window_item'; + +describe('BlockedWindowItem', () => { + const props = { blockedWindow }; + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiComboBox)).toHaveLength(1); + expect(wrapper.find(EuiSuperSelect)).toHaveLength(1); + expect(wrapper.find(EuiDatePicker)).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.tsx new file mode 100644 index 000000000000..80f4244101fd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + EuiButton, + EuiComboBox, + EuiComboBoxOptionOption, + EuiDatePicker, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; + +import { DAYS_OF_WEEK_LABELS } from '../../../../../shared/constants'; +import { BLOCK_LABEL, BETWEEN_LABEL, EVERY_LABEL, AND, REMOVE_BUTTON } from '../../../../constants'; +import { BlockedWindow, DAYS_OF_WEEK_VALUES } from '../../../../types'; + +import { + FULL_SYNC_LABEL, + INCREMENTAL_SYNC_LABEL, + DELETION_SYNC_LABEL, + PERMISSIONS_SYNC_LABEL, + FULL_SYNC_DESCRIPTION, + INCREMENTAL_SYNC_DESCRIPTION, + DELETION_SYNC_DESCRIPTION, + PERMISSIONS_SYNC_DESCRIPTION, +} from '../../constants'; + +interface Props { + blockedWindow: BlockedWindow; +} + +const syncOptions = [ + { + value: 'full', + inputDisplay: FULL_SYNC_LABEL, + dropdownDisplay: ( + <> + {FULL_SYNC_LABEL} + {FULL_SYNC_DESCRIPTION} + + ), + }, + { + value: 'incremental', + inputDisplay: INCREMENTAL_SYNC_LABEL, + dropdownDisplay: ( + <> + {INCREMENTAL_SYNC_LABEL} + {INCREMENTAL_SYNC_DESCRIPTION} + + ), + }, + { + value: 'deletion', + inputDisplay: DELETION_SYNC_LABEL, + dropdownDisplay: ( + <> + {DELETION_SYNC_LABEL} + {DELETION_SYNC_DESCRIPTION} + + ), + }, + { + value: 'permissions', + inputDisplay: PERMISSIONS_SYNC_LABEL, + dropdownDisplay: ( + <> + {PERMISSIONS_SYNC_LABEL} + {PERMISSIONS_SYNC_DESCRIPTION} + + ), + }, +]; + +const dayPickerOptions = DAYS_OF_WEEK_VALUES.reduce((options, day) => { + options.push({ + label: DAYS_OF_WEEK_LABELS[day.toUpperCase() as keyof typeof DAYS_OF_WEEK_LABELS], + value: day, + }); + return options; +}, [] as Array>); + +export const BlockedWindowItem: React.FC = ({ blockedWindow }) => { + const handleSyncTypeChange = () => '#TODO'; + const handleStartDateChange = () => '#TODO'; + const handleEndDateChange = () => '#TODO'; + + return ( + <> + + + + {BLOCK_LABEL} + + + + + + {BETWEEN_LABEL} + + + + + + {AND} + + + + + + {EVERY_LABEL} + + + + + + + {REMOVE_BUTTON} + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.test.tsx new file mode 100644 index 000000000000..0d5183b5e95e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; +import { blockedWindow } from './__mocks__/syncronization.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; + +import { BlockedWindowItem } from './blocked_window_item'; +import { BlockedWindows } from './blocked_window_tab'; + +describe('BlockedWindows', () => { + const addBlockedWindow = jest.fn(); + const mockActions = { + addBlockedWindow, + }; + const mockValues = { + blockedWindows: [blockedWindow], + }; + + beforeEach(() => { + setMockActions(mockActions); + setMockValues(mockValues); + }); + + it('renders blocked windows', () => { + const wrapper = shallow(); + + expect(wrapper.find(BlockedWindowItem)).toHaveLength(1); + }); + + it('renders empty state', () => { + setMockValues({ blockedWindows: [] }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + + it('handles button click', () => { + const wrapper = shallow(); + wrapper.find(EuiButton).simulate('click'); + + expect(addBlockedWindow).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.tsx new file mode 100644 index 000000000000..474bf4cab2a8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { EuiButton, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; + +import { ADD_LABEL } from '../../../../constants'; +import { BLOCKED_EMPTY_STATE_TITLE, BLOCKED_EMPTY_STATE_DESCRIPTION } from '../../constants'; + +import { BlockedWindowItem } from './blocked_window_item'; +import { SynchronizationLogic } from './synchronization_logic'; + +export const BlockedWindows: React.FC = () => { + const { blockedWindows } = useValues(SynchronizationLogic); + const { addBlockedWindow } = useActions(SynchronizationLogic); + + const hasBlockedWindows = blockedWindows.length > 0; + + const emptyState = ( + <> + + {BLOCKED_EMPTY_STATE_TITLE}} + body={

{BLOCKED_EMPTY_STATE_DESCRIPTION}

} + actions={ + + {ADD_LABEL} + + } + /> + + ); + + const blockedWindowItems = ( + <> + {blockedWindows.map((blockedWindow, i) => ( + + ))} + + {ADD_LABEL} + + ); + + return hasBlockedWindows ? blockedWindowItems : emptyState; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.test.tsx new file mode 100644 index 000000000000..08de4b41758a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiTabbedContent } from '@elastic/eui'; + +import { SourceLayout } from '../source_layout'; + +import { Frequency } from './frequency'; + +describe('Frequency', () => { + const handleSelectedTabChanged = jest.fn(); + const mockActions = { + handleSelectedTabChanged, + }; + const mockValues = {}; + + beforeEach(() => { + setMockActions(mockActions); + setMockValues(mockValues); + }); + + it('renders first tab', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTabbedContent)).toHaveLength(1); + expect(wrapper.find(SourceLayout).prop('pageChrome')).toEqual(['Frequency', 'Sync frequency']); + }); + + it('renders second tab', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTabbedContent)).toHaveLength(1); + expect(wrapper.find(SourceLayout).prop('pageChrome')).toEqual([ + 'Frequency', + 'Blocked time windows', + ]); + }); + + describe('tabbed content', () => { + const tabs = [ + { + id: 'source_sync_frequency', + name: 'Sync frequency', + content: <>, + }, + { + id: 'blocked_time_windows', + name: 'Blocked time windows', + content: <>, + }, + ]; + + it('handles first tab click', () => { + const wrapper = shallow(); + const tabsEl = wrapper.find(EuiTabbedContent); + tabsEl.prop('onTabClick')!(tabs[0]); + + expect(handleSelectedTabChanged).toHaveBeenCalledWith('source_sync_frequency'); + }); + + it('handles second tab click', () => { + const wrapper = shallow(); + const tabsEl = wrapper.find(EuiTabbedContent); + tabsEl.prop('onTabClick')!(tabs[1]); + + expect(handleSelectedTabChanged).toHaveBeenCalledWith('blocked_time_windows'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx new file mode 100644 index 000000000000..fb19c84ecfdd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiSpacer, + EuiTabbedContent, + EuiTabbedContentTab, +} from '@elastic/eui'; + +import { SAVE_BUTTON_LABEL } from '../../../../../shared/constants'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { NAV, RESET_BUTTON } from '../../../../constants'; +import { DIFFERENT_SYNC_TYPES_DOCS_URL, SYNC_BEST_PRACTICES_DOCS_URL } from '../../../../routes'; +import { + SOURCE_FREQUENCY_DESCRIPTION, + SOURCE_SYNC_FREQUENCY_TITLE, + BLOCKED_TIME_WINDOWS_TITLE, + DIFFERENT_SYNC_TYPES_LINK_LABEL, + SYNC_BEST_PRACTICES_LINK_LABEL, +} from '../../constants'; +import { SourceLayout } from '../source_layout'; + +import { BlockedWindows } from './blocked_window_tab'; +import { SyncFrequency } from './sync_frequency_tab'; +import { SynchronizationLogic, TabId } from './synchronization_logic'; + +interface FrequencyProps { + tabId: number; +} + +export const Frequency: React.FC = ({ tabId }) => { + const { handleSelectedTabChanged } = useActions(SynchronizationLogic); + + const tabs = [ + { + id: 'source_sync_frequency', + name: SOURCE_SYNC_FREQUENCY_TITLE, + content: , + }, + { + id: 'blocked_time_windows', + name: BLOCKED_TIME_WINDOWS_TITLE, + content: , + }, + ] as EuiTabbedContentTab[]; + + const onSelectedTabChanged = (tab: EuiTabbedContentTab) => { + handleSelectedTabChanged(tab.id as TabId); + }; + + const actions = ( + + + {RESET_BUTTON}{' '} + + + {SAVE_BUTTON_LABEL}{' '} + + + ); + + const docsLinks = ( + + + + {DIFFERENT_SYNC_TYPES_LINK_LABEL} + + + + + {SYNC_BEST_PRACTICES_LINK_LABEL} + + + + ); + + return ( + + + {docsLinks} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency_item.test.tsx new file mode 100644 index 000000000000..fb346ad96117 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency_item.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFieldNumber, EuiSuperSelect } from '@elastic/eui'; + +import { FrequencyItem } from './frequency_item'; + +describe('FrequencyItem', () => { + const props = { + label: 'Item', + description: 'My item', + duration: 'PT2D', + estimate: { + duration: 'PT3D', + nextStart: '2021-09-27T21:39:24+00:00', + lastRun: '2021-09-25T21:39:24+00:00', + }, + }; + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiSuperSelect)).toHaveLength(1); + expect(wrapper.find(EuiFieldNumber)).toHaveLength(1); + }); + + describe('ISO8601 formatting', () => { + it('handles minutes display', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldNumber).prop('value')).toEqual(1563); + expect(wrapper.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('minutes'); + }); + + it('handles hours display', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldNumber).prop('value')).toEqual(26); + expect(wrapper.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('hours'); + }); + + it('handles days display', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldNumber).prop('value')).toEqual(3); + expect(wrapper.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('days'); + }); + + it('handles seconds display (defaults to 1 minute)', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldNumber).prop('value')).toEqual(1); + expect(wrapper.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('minutes'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency_item.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency_item.tsx new file mode 100644 index 000000000000..4e9eec28dc1e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency_item.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import moment from 'moment'; + +import { + EuiFlexGroup, + EuiFieldNumber, + EuiFlexItem, + EuiIconTip, + EuiSpacer, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + MINUTES_UNIT_LABEL, + HOURS_UNIT_LABEL, + DAYS_UNIT_LABEL, +} from '../../../../../shared/constants'; +import { SyncEstimate } from '../../../../types'; + +interface Props { + label: string; + description: string; + duration: string; + estimate: SyncEstimate; +} + +const unitOptions = [ + { + value: 'minutes', + inputDisplay: MINUTES_UNIT_LABEL, + }, + { + value: 'hours', + inputDisplay: HOURS_UNIT_LABEL, + }, + { + value: 'days', + inputDisplay: DAYS_UNIT_LABEL, + }, +]; + +export const FrequencyItem: React.FC = ({ label, description, duration, estimate }) => { + const [interval, unit] = formatDuration(duration); + const { lastRun, nextStart, duration: durationEstimate } = estimate; + const estimateDisplay = durationEstimate && moment.duration(durationEstimate).humanize(); + + const onChange = () => '#TODO'; + + const frequencyItemLabel = ( + {label}, + }} + /> + ); + + const lastRunSummary = ( + + + + ), + lastRunTime: moment(lastRun).fromNow(), + }} + /> + ); + + const nextStartSummary = ( + + + + ), + nextStartTime: moment(nextStart).fromNow(), + }} + /> + ); + + const estimateSummary = ( + + ); + + return ( + <> + + + {frequencyItemLabel} + + + + + + + + + + + + + + {lastRun && lastRunSummary} {nextStartSummary} {estimateDisplay && estimateSummary} + + + + ); +}; + +// In most cases, the user will use the form to set the sync frequency, in which case the duration +// will be in the format of "PT3D" (ISO 8601). However, if an operator has set the sync frequency via +// the API, the duration could be a complex format, such as "P1DT2H3M4S". It was decided that in this +// case, we should omit seconds and go with the least common denominator from minutes. +// +// Example: "P1DT2H3M4S" -> "1563 Minutes" +const formatDuration = (duration: string): [interval: number, unit: string] => { + const momentDuration = moment.duration(duration); + if (duration.includes('M')) { + return [Math.round(momentDuration.asMinutes()), unitOptions[0].value]; + } + if (duration.includes('H')) { + return [Math.round(momentDuration.asHours()), unitOptions[1].value]; + } + if (duration.includes('D')) { + return [Math.round(momentDuration.asDays()), unitOptions[2].value]; + } + + return [1, unitOptions[0].value]; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/index.ts new file mode 100644 index 000000000000..450bc41bf46f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { SynchronizationRouter } from './synchronization_router'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx new file mode 100644 index 000000000000..4c2804459f1b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiHorizontalRule, EuiLink } from '@elastic/eui'; + +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { NAV } from '../../../../constants'; +import { OBJECTS_AND_ASSETS_DOCS_URL } from '../../../../routes'; +import { + SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION, + SYNC_OBJECTS_TYPES_LINK_LABEL, +} from '../../constants'; +import { SourceLayout } from '../source_layout'; + +export const ObjectsAndAssets: React.FC = () => { + return ( + + + + {SYNC_OBJECTS_TYPES_LINK_LABEL} + + +
TODO
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.test.tsx new file mode 100644 index 000000000000..6c382b0addab --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; +import { setMockValues } from '../../../../../__mocks__/kea_logic'; +import { fullContentSources } from '../../../../__mocks__/content_sources.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; +import { cloneDeep } from 'lodash'; + +import { FrequencyItem } from './frequency_item'; +import { SyncFrequency } from './sync_frequency_tab'; + +describe('SyncFrequency', () => { + const contentSource = fullContentSources[0]; + const sourceWithNoDLP = cloneDeep(contentSource); + sourceWithNoDLP.indexing.schedule.permissions = undefined as any; + sourceWithNoDLP.indexing.schedule.estimates.permissions = undefined as any; + + it('renders with DLP', () => { + setMockValues({ contentSource }); + const wrapper = shallow(); + + expect(wrapper.find(FrequencyItem)).toHaveLength(4); + }); + + it('renders without DLP', () => { + setMockValues({ + contentSource: sourceWithNoDLP, + }); + const wrapper = shallow(); + + expect(wrapper.find(FrequencyItem)).toHaveLength(3); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.tsx new file mode 100644 index 000000000000..2a0ccb1fdb2c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; + +import { + FULL_SYNC_LABEL, + INCREMENTAL_SYNC_LABEL, + DELETION_SYNC_LABEL, + PERMISSIONS_SYNC_LABEL, + FULL_SYNC_DESCRIPTION, + INCREMENTAL_SYNC_DESCRIPTION, + DELETION_SYNC_DESCRIPTION, + PERMISSIONS_SYNC_DESCRIPTION, +} from '../../constants'; +import { SourceLogic } from '../../source_logic'; + +import { FrequencyItem } from './frequency_item'; + +export const SyncFrequency: React.FC = () => { + const { + contentSource: { + indexing: { + schedule: { + full: fullDuration, + incremental: incrementalDuration, + delete: deleteDuration, + permissions: permissionsDuration, + estimates: { + full: fullEstimate, + incremental: incrementalEstimate, + delete: deleteEstimate, + permissions: permissionsEstimate, + }, + }, + }, + }, + } = useValues(SourceLogic); + + return ( + <> + + + + + + + {permissionsDuration && permissionsEstimate && ( + <> + + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.test.tsx new file mode 100644 index 000000000000..632af08611ca --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiLink, EuiCallOut, EuiSwitch } from '@elastic/eui'; + +import { Synchronization } from './synchronization'; + +describe('Synchronization', () => { + it('renders when config enabled', () => { + setMockValues({ contentSource: { isSyncConfigEnabled: true } }); + const wrapper = shallow(); + + expect(wrapper.find(EuiLink)).toHaveLength(1); + expect(wrapper.find(EuiSwitch)).toHaveLength(1); + }); + + it('renders when config disabled', () => { + setMockValues({ contentSource: { isSyncConfigEnabled: false } }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx new file mode 100644 index 000000000000..21daee8f26d4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiCallOut, EuiLink, EuiPanel, EuiSwitch, EuiSpacer, EuiText } from '@elastic/eui'; + +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { NAV } from '../../../../constants'; +import { SYNCHRONIZATION_DOCS_URL } from '../../../../routes'; +import { + SOURCE_SYNCRONIZATION_DESCRIPTION, + SYNCHRONIZATION_DISABLED_TITLE, + SYNCHRONIZATION_DISABLED_DESCRIPTION, + SOURCE_SYNCRONIZATION_TOGGLE_LABEL, + SOURCE_SYNCRONIZATION_TOGGLE_DESCRIPTION, + SYNCHRONIZATION_LINK_LABEL, +} from '../../constants'; +import { SourceLogic } from '../../source_logic'; +import { SourceLayout } from '../source_layout'; + +export const Synchronization: React.FC = () => { + const { + contentSource: { isSyncConfigEnabled }, + } = useValues(SourceLogic); + + const onChange = (checked: boolean) => `#TODO: ${checked}`; + const syncToggle = ( + + onChange(e.target.checked)} + /> + + + {SOURCE_SYNCRONIZATION_TOGGLE_DESCRIPTION} + + + ); + + const syncDisabledCallout = ( + +

{SYNCHRONIZATION_DISABLED_DESCRIPTION}

+
+ ); + + return ( + + + + {SYNCHRONIZATION_LINK_LABEL} + + + {isSyncConfigEnabled ? syncToggle : syncDisabledCallout} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts new file mode 100644 index 000000000000..50553d149341 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter, mockKibanaValues } from '../../../../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test/jest'; + +const contentSource = { id: 'source123' }; +jest.mock('../../source_logic', () => ({ + SourceLogic: { values: { contentSource } }, +})); + +jest.mock('../../../../app_logic', () => ({ + AppLogic: { values: { isOrganization: true } }, +})); + +import { SynchronizationLogic, emptyBlockedWindow } from './synchronization_logic'; + +describe('SynchronizationLogic', () => { + const { navigateToUrl } = mockKibanaValues; + const { mount } = new LogicMounter(SynchronizationLogic); + + const defaultValues = { + navigatingBetweenTabs: false, + blockedWindows: [], + }; + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(SynchronizationLogic.values).toEqual(defaultValues); + }); + + describe('actions', () => { + it('setNavigatingBetweenTabs', () => { + SynchronizationLogic.actions.setNavigatingBetweenTabs(true); + + expect(SynchronizationLogic.values.navigatingBetweenTabs).toEqual(true); + }); + + it('addBlockedWindow', () => { + SynchronizationLogic.actions.addBlockedWindow(); + + expect(SynchronizationLogic.values.blockedWindows).toEqual([emptyBlockedWindow]); + }); + }); + + describe('listeners', () => { + describe('handleSelectedTabChanged', () => { + it('calls setNavigatingBetweenTabs', async () => { + const setNavigatingBetweenTabsSpy = jest.spyOn( + SynchronizationLogic.actions, + 'setNavigatingBetweenTabs' + ); + SynchronizationLogic.actions.handleSelectedTabChanged('source_sync_frequency'); + await nextTick(); + + expect(setNavigatingBetweenTabsSpy).toHaveBeenCalledWith(true); + expect(navigateToUrl).toHaveBeenCalledWith('/sources/source123/synchronization/frequency'); + }); + + it('calls calls correct route for "blocked_time_windows"', async () => { + SynchronizationLogic.actions.handleSelectedTabChanged('blocked_time_windows'); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalledWith( + '/sources/source123/synchronization/frequency/blocked_windows' + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.ts new file mode 100644 index 000000000000..4f67f6471e6e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; +import moment from 'moment'; + +export type TabId = 'source_sync_frequency' | 'blocked_time_windows'; + +import { KibanaLogic } from '../../../../../shared/kibana'; +import { AppLogic } from '../../../../app_logic'; +import { + SYNC_FREQUENCY_PATH, + BLOCKED_TIME_WINDOWS_PATH, + getContentSourcePath, +} from '../../../../routes'; +import { BlockedWindow } from '../../../../types'; + +import { SourceLogic } from '../../source_logic'; + +interface SynchronizationActions { + setNavigatingBetweenTabs(navigatingBetweenTabs: boolean): boolean; + handleSelectedTabChanged(tabId: TabId): TabId; + addBlockedWindow(): void; +} + +interface SynchronizationValues { + hasUnsavedChanges: boolean; + navigatingBetweenTabs: boolean; + blockedWindows: BlockedWindow[]; +} + +export const emptyBlockedWindow: BlockedWindow = { + jobType: 'full', + day: 'monday', + start: moment().set('hour', 11).set('minutes', 0), + end: moment().set('hour', 13).set('minutes', 0), +}; + +export const SynchronizationLogic = kea< + MakeLogicType +>({ + actions: { + setNavigatingBetweenTabs: (navigatingBetweenTabs: boolean) => navigatingBetweenTabs, + handleSelectedTabChanged: (tabId: TabId) => tabId, + addBlockedWindow: true, + }, + reducers: { + navigatingBetweenTabs: [ + false, + { + setNavigatingBetweenTabs: (_, navigatingBetweenTabs) => navigatingBetweenTabs, + }, + ], + blockedWindows: [ + [], + { + addBlockedWindow: (state, _) => [...state, emptyBlockedWindow], + }, + ], + }, + listeners: ({ actions }) => ({ + handleSelectedTabChanged: async (tabId, breakpoint) => { + const { isOrganization } = AppLogic.values; + const { id: sourceId } = SourceLogic.values.contentSource; + const path = + tabId === 'source_sync_frequency' + ? getContentSourcePath(SYNC_FREQUENCY_PATH, sourceId, isOrganization) + : getContentSourcePath(BLOCKED_TIME_WINDOWS_PATH, sourceId, isOrganization); + + // This method is needed because the shared `UnsavedChangesPrompt` component is triggered + // when navigating between tabs. We set a boolean flag that tells the prompt there are no + // unsaved changes when navigating between the tabs and reset it one the transition is complete + // in order to restore the intended functionality when navigating away with unsaved changes. + actions.setNavigatingBetweenTabs(true); + + await breakpoint(); + + KibanaLogic.values.navigateToUrl(path); + actions.setNavigatingBetweenTabs(false); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.test.tsx new file mode 100644 index 000000000000..cf130d2c21a5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { shallow } from 'enzyme'; + +import { Frequency } from './frequency'; +import { ObjectsAndAssets } from './objects_and_assets'; +import { Synchronization } from './synchronization'; +import { SynchronizationRouter } from './synchronization_router'; + +describe('SynchronizationRouter', () => { + it('renders', () => { + setMockValues({ isOrganization: true }); + const wrapper = shallow(); + + expect(wrapper.find(Synchronization)).toHaveLength(1); + expect(wrapper.find(ObjectsAndAssets)).toHaveLength(1); + expect(wrapper.find(Frequency)).toHaveLength(2); + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(4); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.tsx new file mode 100644 index 000000000000..ede0f293377c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { + SYNC_FREQUENCY_PATH, + BLOCKED_TIME_WINDOWS_PATH, + OBJECTS_AND_ASSETS_PATH, + SOURCE_SYNCHRONIZATION_PATH, + getSourcesPath, +} from '../../../../routes'; + +import { Frequency } from './frequency'; +import { ObjectsAndAssets } from './objects_and_assets'; +import { Synchronization } from './synchronization'; + +export const SynchronizationRouter: React.FC = () => ( + + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.test.tsx new file mode 100644 index 000000000000..a2978c34475d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.test.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../../__mocks__/kea_logic'; + +jest.mock('../../../../../shared/layout', () => ({ + generateNavLink: jest.fn(({ to }) => ({ href: to })), +})); + +import { useSynchronizationSubNav } from './synchronization_sub_nav'; + +describe('useSynchronizationSubNav', () => { + it('renders nav items', () => { + setMockValues({ contentSource: { id: '1', isSyncConfigEnabled: true } }); + + expect(useSynchronizationSubNav()).toEqual([ + { + id: 'sourceSynchronizationFrequency', + name: 'Frequency', + href: '/sources/1/synchronization/frequency', + }, + { + id: 'sourceSynchronizationObjectsAndAssets', + name: 'Objects and assets', + href: '/sources/1/synchronization/objects_and_assets', + }, + ]); + }); + + it('returns undefined when no content source id is present', () => { + setMockValues({ contentSource: {} }); + + expect(useSynchronizationSubNav()).toEqual(undefined); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.tsx new file mode 100644 index 000000000000..2df6e9177211 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useValues } from 'kea'; + +import { EuiSideNavItemType } from '@elastic/eui'; + +import { generateNavLink } from '../../../../../shared/layout'; +import { NAV } from '../../../../constants'; +import { + getContentSourcePath, + SYNC_FREQUENCY_PATH, + OBJECTS_AND_ASSETS_PATH, +} from '../../../../routes'; +import { SourceLogic } from '../../source_logic'; + +export const useSynchronizationSubNav = () => { + const { + contentSource: { id, isSyncConfigEnabled }, + } = useValues(SourceLogic); + + if (!id || !isSyncConfigEnabled) return undefined; + + const navItems: Array> = [ + { + id: 'sourceSynchronizationFrequency', + name: NAV.SYNCHRONIZATION_FREQUENCY, + ...generateNavLink({ + to: getContentSourcePath(SYNC_FREQUENCY_PATH, id, true), + shouldShowActiveForSubroutes: true, + }), + }, + { + id: 'sourceSynchronizationObjectsAndAssets', + name: NAV.SYNCHRONIZATION_OBJECTS_AND_ASSETS, + ...generateNavLink({ to: getContentSourcePath(OBJECTS_AND_ASSETS_PATH, id, true) }), + }, + ]; + + return navItems; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts index b90d6abe3870..ae55a970a4f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts @@ -540,3 +540,174 @@ export const SOURCE_OVERVIEW_TITLE = i18n.translate( defaultMessage: 'Source overview', } ); + +export const SOURCE_SYNCRONIZATION_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncronizationDescription', + { + defaultMessage: + 'DO NOT TRANSLATE, temporary placeholder: Sync chupa chups dragée gummi bears jelly beans brownie. Fruitcake pie chocolate cake caramels carrot cake cotton candy dragée sweet roll soufflé.', + } +); + +export const SOURCE_FREQUENCY_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceFrequencyDescription', + { + defaultMessage: + 'DO NOT TRANSLATE, temporary placeholder: Frequency chupa chups dragée gummi bears jelly beans brownie. Fruitcake pie chocolate cake caramels carrot cake cotton candy dragée sweet roll soufflé.', + } +); + +export const SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsDescription', + { + defaultMessage: + 'DO NOT TRANSLATE, temporary placeholder: Objects chupa chups dragée gummi bears jelly beans brownie. Fruitcake pie chocolate cake caramels carrot cake cotton candy dragée sweet roll soufflé.', + } +); + +export const SOURCE_SYNCRONIZATION_TOGGLE_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncronizationToggleLabel', + { + defaultMessage: 'Synchronize this source', + } +); + +export const SOURCE_SYNCRONIZATION_TOGGLE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncronizationToggleDescription', + { + defaultMessage: 'Source content will automatically be kept in sync.', + } +); + +export const SOURCE_SYNCRONIZATION_FREQUENCY_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncronizationFrequencyTitle', + { + defaultMessage: 'Syncronization frequency', + } +); + +export const SOURCE_SYNC_FREQUENCY_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncFrequencyTitle', + { + defaultMessage: 'Sync frequency', + } +); + +export const BLOCKED_TIME_WINDOWS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.blockedWindowsTitle', + { + defaultMessage: 'Blocked time windows', + } +); + +export const SYNCHRONIZATION_LINK_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.synchronizationLinkLabel', + { + defaultMessage: 'Learn more about synchronization', + } +); + +export const SYNCHRONIZATION_DISABLED_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.synchronizationDisabledTitle', + { + defaultMessage: 'Source synchronization is disabled.', + } +); + +export const SYNCHRONIZATION_DISABLED_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.synchronizationDisabledDescription', + { + defaultMessage: 'Contact your administrator to enable synchronization controls.', + } +); + +export const DIFFERENT_SYNC_TYPES_LINK_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.differentSyncTypesLinkLabel', + { + defaultMessage: 'Learn more about different sync types', + } +); + +export const SYNC_BEST_PRACTICES_LINK_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.syncBestPracticesLinkLabel', + { + defaultMessage: 'Learn more about sync best practices', + } +); + +export const SYNC_OBJECTS_TYPES_LINK_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.syncObjectsTypesLinkLabel', + { + defaultMessage: 'Learn more about sync objects types', + } +); + +export const FULL_SYNC_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.fullSyncLabel', + { + defaultMessage: 'Full sync', + } +); + +export const FULL_SYNC_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.fullSyncDescription', + { + defaultMessage: 'Retrieves all the documents possible from the content source', + } +); + +export const INCREMENTAL_SYNC_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.incrementalSyncLabel', + { + defaultMessage: 'Incremental sync', + } +); + +export const INCREMENTAL_SYNC_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.incrementalSyncDescription', + { + defaultMessage: 'Retrieves documents/updates since the last sync job', + } +); + +export const DELETION_SYNC_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.deletionSyncLabel', + { + defaultMessage: 'Deletion sync', + } +); + +export const DELETION_SYNC_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.deletionSyncDescription', + { + defaultMessage: 'Removes documents that no longer exist in the content source', + } +); + +export const PERMISSIONS_SYNC_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.permissionsSyncLabel', + { + defaultMessage: 'Permissions sync', + } +); + +export const PERMISSIONS_SYNC_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.permissionsSyncDescription', + { + defaultMessage: 'Retrieves all permission changes since the last sync job', + } +); + +export const BLOCKED_EMPTY_STATE_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.blockedEmptyStateTitle', + { + defaultMessage: 'You have no blocked time windows', + } +); + +export const BLOCKED_EMPTY_STATE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.blockedEmptyStateDescription', + { + defaultMessage: 'Add a blocked time window to only perform syncs at the right time.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index fbc8eb159a7a..3f9bbc59e842 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -90,7 +90,7 @@ describe('SourceRouter', () => { expect(wrapper.find(Overview)).toHaveLength(1); expect(wrapper.find(SourceSettings)).toHaveLength(1); expect(wrapper.find(SourceContent)).toHaveLength(1); - expect(wrapper.find(Route)).toHaveLength(4); + expect(wrapper.find(Route)).toHaveLength(5); }); it('renders source routes (custom)', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index 2a7dc2ebcc28..6cb9ea6bc681 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -21,6 +21,7 @@ import { SOURCE_SCHEMAS_PATH, SOURCE_DISPLAY_SETTINGS_PATH, SOURCE_SETTINGS_PATH, + SOURCE_SYNCHRONIZATION_PATH, getContentSourcePath as sourcePath, getSourcesPath, } from '../../routes'; @@ -32,6 +33,7 @@ import { Schema } from './components/schema'; import { SchemaChangeErrors } from './components/schema/schema_change_errors'; import { SourceContent } from './components/source_content'; import { SourceSettings } from './components/source_settings'; +import { SynchronizationRouter } from './components/synchronization'; import { SourceLogic } from './source_logic'; export const SourceRouter: React.FC = () => { @@ -59,6 +61,7 @@ export const SourceRouter: React.FC = () => { const { serviceType } = contentSource; const isCustomSource = serviceType === CUSTOM_SERVICE_TYPE; + const showSynchronization = !isCustomSource && isOrganization; return ( @@ -68,6 +71,11 @@ export const SourceRouter: React.FC = () => { + {showSynchronization && ( + + + + )} {isCustomSource && ( diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts index d6f741526b29..2bdcfb9fe9d5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts @@ -38,6 +38,35 @@ describe('search relevance insights routes', () => { }); }); + describe('PUT /internal/app_search/engines/{name}/search_relevance_suggestions', () => { + const mockRouter = new MockRouter({ + method: 'put', + path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions', + }); + + beforeEach(() => { + registerSearchRelevanceSuggestionsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine' }, + body: { + query: 'some query', + type: 'curation', + status: 'applied', + }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/search_relevance_suggestions', + }); + }); + }); + describe('GET /internal/app_search/engines/{name}/search_relevance_suggestions/settings', () => { const mockRouter = new MockRouter({ method: 'get', @@ -86,4 +115,28 @@ describe('search relevance insights routes', () => { }); }); }); + + describe('POST /internal/app_search/engines/{name}/search_relevance_suggestions/{query}', () => { + const mockRouter = new MockRouter({ + method: 'post', + path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/{query}', + }); + + beforeEach(() => { + registerSearchRelevanceSuggestionsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine', query: 'foo' }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/search_relevance_suggestions/:query', + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts index 861d8c52b537..8b3b204c24d7 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts @@ -39,6 +39,20 @@ export function registerSearchRelevanceSuggestionsRoutes({ }) ); + router.put( + skipBodyValidation({ + path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + }, + }), + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/search_relevance_suggestions', + }) + ); + router.get( { path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/settings', @@ -66,4 +80,29 @@ export function registerSearchRelevanceSuggestionsRoutes({ path: '/api/as/v0/engines/:engineName/search_relevance_suggestions/settings', }) ); + + router.post( + { + path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/{query}', + validate: { + params: schema.object({ + engineName: schema.string(), + query: schema.string(), + }), + body: schema.object({ + page: schema.object({ + current: schema.number(), + size: schema.number(), + }), + filters: schema.object({ + status: schema.arrayOf(schema.string()), + type: schema.string(), + }), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/search_relevance_suggestions/:query', + }) + ); } diff --git a/x-pack/plugins/fleet/common/constants/agent_policy.ts b/x-pack/plugins/fleet/common/constants/agent_policy.ts index d0bde39e9f2d..95078d8ead84 100644 --- a/x-pack/plugins/fleet/common/constants/agent_policy.ts +++ b/x-pack/plugins/fleet/common/constants/agent_policy.ts @@ -11,3 +11,17 @@ export const agentPolicyStatuses = { Active: 'active', Inactive: 'inactive', } as const; + +export const AGENT_POLICY_DEFAULT_MONITORING_DATASETS = [ + 'elastic_agent', + 'elastic_agent.elastic_agent', + 'elastic_agent.apm_server', + 'elastic_agent.filebeat', + 'elastic_agent.fleet_server', + 'elastic_agent.metricbeat', + 'elastic_agent.osquerybeat', + 'elastic_agent.packetbeat', + 'elastic_agent.endpoint_security', + 'elastic_agent.auditbeat', + 'elastic_agent.heartbeat', +]; diff --git a/x-pack/plugins/fleet/jest.config.js b/x-pack/plugins/fleet/jest.config.js index 5443318d52c8..0841e48e5946 100644 --- a/x-pack/plugins/fleet/jest.config.js +++ b/x-pack/plugins/fleet/jest.config.js @@ -9,6 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/fleet'], + transform: { + '^.+\\.stories\\.tsx?$': '@storybook/addon-storyshots/injectFileName', + }, coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/fleet', coverageReporters: ['text', 'html'], collectCoverageFrom: ['/x-pack/plugins/fleet/{common,public,server}/**/*.{ts,tsx}'], diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx index 398421278b72..954addd4202b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx @@ -17,12 +17,17 @@ import { EuiFieldPassword, EuiCodeBlock, } from '@elastic/eui'; +import styled from 'styled-components'; import type { RegistryVarsEntry } from '../../../../types'; import { CodeEditor } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { MultiTextInput } from './multi_text_input'; +const FixedHeightDiv = styled.div` + height: 300px; +`; + export const PackagePolicyInputVarField: React.FunctionComponent<{ varDef: RegistryVarsEntry; value: any; @@ -55,31 +60,34 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{
{value}
) : ( - + + + ); case 'bool': return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx index 1d43f90b80de..a8cab77af447 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx @@ -31,7 +31,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { DownloadStep } from '../../../../components'; import { useStartServices, - useGetOutputs, + useDefaultOutput, sendGenerateServiceToken, usePlatform, PLATFORM_OPTIONS, @@ -242,7 +242,7 @@ export const FleetServerCommandStep = ({ }; export const useFleetServerInstructions = (policyId?: string) => { - const outputsRequest = useGetOutputs(); + const { output, refresh: refreshOutputs } = useDefaultOutput(); const { notifications } = useStartServices(); const [serviceToken, setServiceToken] = useState(); const [isLoadingServiceToken, setIsLoadingServiceToken] = useState(false); @@ -250,9 +250,7 @@ export const useFleetServerInstructions = (policyId?: string) => { const [deploymentMode, setDeploymentMode] = useState('production'); const { data: settings, resendRequest: refreshSettings } = useGetSettings(); const fleetServerHost = settings?.item.fleet_server_hosts?.[0]; - const output = outputsRequest.data?.items?.[0]; const esHost = output?.hosts?.[0]; - const refreshOutputs = outputsRequest.resendRequest; const installCommand = useMemo((): string => { if (!serviceToken || !esHost) { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx index 737a83d5f5da..f3bf7106fabc 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx @@ -66,7 +66,7 @@ export const List = (props: Args) => ( description: 'Not Installed Description', name: 'beats', release: 'ga', - id: 'id', + id: 'package_one', version: '1.0.0', uiInternalPath: '/', path: 'path', @@ -77,7 +77,7 @@ export const List = (props: Args) => ( description: 'Not Installed Description', name: 'aws', release: 'beta', - id: 'id', + id: 'package_two', version: '1.0.0', uiInternalPath: '/', path: 'path', @@ -88,7 +88,7 @@ export const List = (props: Args) => ( description: 'Not Installed Description', name: 'azure', release: 'experimental', - id: 'id', + id: 'package_three', version: '1.0.0', uiInternalPath: '/', path: 'path', @@ -99,36 +99,45 @@ export const List = (props: Args) => ( description: 'Installed Description', name: 'elastic', release: 'ga', - id: 'id', + id: 'package_four', version: '1.0.0', uiInternalPath: '/', path: 'path', status: 'installed', - savedObject, + savedObject: { + ...savedObject, + id: 'package_four', + }, }, { title: 'Package Five', description: 'Installed Description', name: 'unknown', release: 'beta', - id: 'id', + id: 'package_five', version: '1.0.0', uiInternalPath: '/', path: 'path', status: 'installed', - savedObject, + savedObject: { + ...savedObject, + id: 'package_five', + }, }, { title: 'Package Six', description: 'Installed Description', name: 'kibana', release: 'experimental', - id: 'id', + id: 'package_six', version: '1.0.0', uiInternalPath: '/', path: 'path', status: 'installed', - savedObject, + savedObject: { + ...savedObject, + id: 'package_six', + }, }, ] as unknown as IntegrationCardItem[] } diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx index 0a601d2128bb..467dae12fa58 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx @@ -97,10 +97,10 @@ export const Details: React.FC = memo(({ packageInfo }) => { ), description: ( - {entries(filteredTypes).map(([_type, parts]) => { + {entries(filteredTypes).map(([_type, parts], index) => { const type = _type as KibanaAssetType; return ( - + {AssetTitleMap[type]} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 42eb68099970..304bdd621b1b 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -217,7 +217,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps }), render(_version, { agentPolicy, packagePolicy }) { return ( - + { isLoadingCategories, ]); - if (!categoryExists(selectedCategory, categories)) { + if (!isLoadingCategories && !categoryExists(selectedCategory, categories)) { history.replace(pagePathGetters.integrations_all({ category: '', searchTerm: searchParam })[1]); return null; } diff --git a/x-pack/plugins/fleet/public/components/header.tsx b/x-pack/plugins/fleet/public/components/header.tsx index d2f713cc7668..80cebe3b0b30 100644 --- a/x-pack/plugins/fleet/public/components/header.tsx +++ b/x-pack/plugins/fleet/public/components/header.tsx @@ -78,8 +78,8 @@ export const Header: React.FC = ({ - {tabs.map((props) => ( - + {tabs.map((props, index) => ( + {props.name} ))} diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx index e42733bbd2da..9bedfca0d3bc 100644 --- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx @@ -36,7 +36,7 @@ import { useGetSettings, useInput, sendPutSettings, - useGetOutputs, + useDefaultOutput, sendPutOutput, } from '../../hooks'; import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../common'; @@ -258,8 +258,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { const settingsRequest = useGetSettings(); const settings = settingsRequest?.data?.item; - const outputsRequest = useGetOutputs(); - const output = outputsRequest.data?.items?.[0]; + const { output } = useDefaultOutput(); const { inputs, submit, validate, isLoading } = useSettingsForm(output?.id, onClose); const [isConfirmModalVisible, setConfirmModalVisible] = React.useState(false); diff --git a/x-pack/plugins/fleet/public/hooks/use_package_icon_type.ts b/x-pack/plugins/fleet/public/hooks/use_package_icon_type.ts index a86c68c00c64..1b21b7bfd78e 100644 --- a/x-pack/plugins/fleet/public/hooks/use_package_icon_type.ts +++ b/x-pack/plugins/fleet/public/hooks/use_package_icon_type.ts @@ -11,7 +11,8 @@ import { ICON_TYPES } from '@elastic/eui'; import type { PackageInfo, PackageListItem } from '../types'; // TODO: Determine whether this can be relocated -import { useLinks as useEPMLinks } from '../applications/integrations/hooks'; +// Import the specific hook to avoid a circular dependency in Babel +import { useLinks as useEPMLinks } from '../applications/integrations/hooks/use_links'; import { sendGetPackageInfoByKey } from './index'; diff --git a/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts b/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts index 0fcaa262cf32..2d623da505c6 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { useMemo, useCallback } from 'react'; + import { outputRoutesService } from '../../services'; import type { PutOutputRequest, GetOutputsResponse } from '../../types'; @@ -17,6 +19,21 @@ export function useGetOutputs() { }); } +export function useDefaultOutput() { + const outputsRequest = useGetOutputs(); + const output = useMemo(() => { + return outputsRequest.data?.items.find((o) => o.is_default); + }, [outputsRequest.data]); + + const refresh = useCallback(() => { + return outputsRequest.resendRequest(); + }, [outputsRequest]); + + return useMemo(() => { + return { output, refresh }; + }, [output, refresh]); +} + export function sendPutOutput(outputId: string, body: PutOutputRequest['body']) { return sendRequest({ method: 'put', diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 0dcd5e7f4780..2ce457242c6b 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -50,6 +50,7 @@ export { DEFAULT_OUTPUT, DEFAULT_PACKAGES, PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, + AGENT_POLICY_DEFAULT_MONITORING_DATASETS, // Fleet Server index FLEET_SERVER_SERVERS_INDEX, ENROLLMENT_API_KEYS_INDEX, diff --git a/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap b/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap index 970bccbafa63..36044d35703e 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap @@ -51,25 +51,14 @@ Object { "cluster": Array [ "monitor", ], + }, + "_elastic_agent_monitoring": Object { "indices": Array [ Object { "names": Array [ - "metrics-elastic_agent-default", - "metrics-elastic_agent.elastic_agent-default", - "metrics-elastic_agent.apm_server-default", - "metrics-elastic_agent.filebeat-default", - "metrics-elastic_agent.fleet_server-default", - "metrics-elastic_agent.metricbeat-default", - "metrics-elastic_agent.osquerybeat-default", - "metrics-elastic_agent.packetbeat-default", - "metrics-elastic_agent.endpoint_security-default", - "metrics-elastic_agent.auditbeat-default", - "metrics-elastic_agent.heartbeat-default", - ], - "privileges": Array [ - "auto_configure", - "create_doc", + "metrics-default", ], + "privileges": Array [], }, ], }, @@ -148,25 +137,14 @@ Object { "cluster": Array [ "monitor", ], + }, + "_elastic_agent_monitoring": Object { "indices": Array [ Object { "names": Array [ - "metrics-elastic_agent-default", - "metrics-elastic_agent.elastic_agent-default", - "metrics-elastic_agent.apm_server-default", - "metrics-elastic_agent.filebeat-default", - "metrics-elastic_agent.fleet_server-default", - "metrics-elastic_agent.metricbeat-default", - "metrics-elastic_agent.osquerybeat-default", - "metrics-elastic_agent.packetbeat-default", - "metrics-elastic_agent.endpoint_security-default", - "metrics-elastic_agent.auditbeat-default", - "metrics-elastic_agent.heartbeat-default", - ], - "privileges": Array [ - "auto_configure", - "create_doc", + "metrics-default", ], + "privileges": Array [], }, ], }, @@ -245,25 +223,14 @@ Object { "cluster": Array [ "monitor", ], + }, + "_elastic_agent_monitoring": Object { "indices": Array [ Object { "names": Array [ - "metrics-elastic_agent-default", - "metrics-elastic_agent.elastic_agent-default", - "metrics-elastic_agent.apm_server-default", - "metrics-elastic_agent.filebeat-default", - "metrics-elastic_agent.fleet_server-default", - "metrics-elastic_agent.metricbeat-default", - "metrics-elastic_agent.osquerybeat-default", - "metrics-elastic_agent.packetbeat-default", - "metrics-elastic_agent.endpoint_security-default", - "metrics-elastic_agent.auditbeat-default", - "metrics-elastic_agent.heartbeat-default", - ], - "privileges": Array [ - "auto_configure", - "create_doc", + "metrics-default", ], + "privileges": Array [], }, ], }, diff --git a/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/monitoring_permissions.test.ts.snap b/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/monitoring_permissions.test.ts.snap new file mode 100644 index 000000000000..a54d4beb6c04 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/monitoring_permissions.test.ts.snap @@ -0,0 +1,195 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getMonitoringPermissions With elastic agent package installed should return default logs and metrics permissions if both are enabled 1`] = ` +Object { + "_elastic_agent_monitoring": Object { + "indices": Array [ + Object { + "names": Array [ + "logs-elastic_agent.metricbeat-testnamespace123", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + Object { + "names": Array [ + "metrics-elastic_agent.metricbeat-testnamespace123", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + Object { + "names": Array [ + "logs-elastic_agent.filebeat-testnamespace123", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + Object { + "names": Array [ + "metrics-elastic_agent.filebeat-testnamespace123", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, +} +`; + +exports[`getMonitoringPermissions With elastic agent package installed should return default logs permissions if only logs are enabled 1`] = ` +Object { + "_elastic_agent_monitoring": Object { + "indices": Array [ + Object { + "names": Array [ + "logs-elastic_agent.metricbeat-testnamespace123", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + Object { + "names": Array [ + "logs-elastic_agent.filebeat-testnamespace123", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, +} +`; + +exports[`getMonitoringPermissions With elastic agent package installed should return default metrics permissions if only metrics are enabled 1`] = ` +Object { + "_elastic_agent_monitoring": Object { + "indices": Array [ + Object { + "names": Array [ + "metrics-elastic_agent.metricbeat-testnamespace123", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + Object { + "names": Array [ + "metrics-elastic_agent.filebeat-testnamespace123", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, +} +`; + +exports[`getMonitoringPermissions Without elastic agent package installed should return default logs and metrics permissions if both are enabled 1`] = ` +Object { + "_elastic_agent_monitoring": Object { + "indices": Array [ + Object { + "names": Array [ + "logs-elastic_agent-testnamespace123", + "logs-elastic_agent.elastic_agent-testnamespace123", + "logs-elastic_agent.apm_server-testnamespace123", + "logs-elastic_agent.filebeat-testnamespace123", + "logs-elastic_agent.fleet_server-testnamespace123", + "logs-elastic_agent.metricbeat-testnamespace123", + "logs-elastic_agent.osquerybeat-testnamespace123", + "logs-elastic_agent.packetbeat-testnamespace123", + "logs-elastic_agent.endpoint_security-testnamespace123", + "logs-elastic_agent.auditbeat-testnamespace123", + "logs-elastic_agent.heartbeat-testnamespace123", + "metrics-elastic_agent-testnamespace123", + "metrics-elastic_agent.elastic_agent-testnamespace123", + "metrics-elastic_agent.apm_server-testnamespace123", + "metrics-elastic_agent.filebeat-testnamespace123", + "metrics-elastic_agent.fleet_server-testnamespace123", + "metrics-elastic_agent.metricbeat-testnamespace123", + "metrics-elastic_agent.osquerybeat-testnamespace123", + "metrics-elastic_agent.packetbeat-testnamespace123", + "metrics-elastic_agent.endpoint_security-testnamespace123", + "metrics-elastic_agent.auditbeat-testnamespace123", + "metrics-elastic_agent.heartbeat-testnamespace123", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, +} +`; + +exports[`getMonitoringPermissions Without elastic agent package installed should return default logs permissions if only logs are enabled 1`] = ` +Object { + "_elastic_agent_monitoring": Object { + "indices": Array [ + Object { + "names": Array [ + "logs-elastic_agent-testnamespace123", + "logs-elastic_agent.elastic_agent-testnamespace123", + "logs-elastic_agent.apm_server-testnamespace123", + "logs-elastic_agent.filebeat-testnamespace123", + "logs-elastic_agent.fleet_server-testnamespace123", + "logs-elastic_agent.metricbeat-testnamespace123", + "logs-elastic_agent.osquerybeat-testnamespace123", + "logs-elastic_agent.packetbeat-testnamespace123", + "logs-elastic_agent.endpoint_security-testnamespace123", + "logs-elastic_agent.auditbeat-testnamespace123", + "logs-elastic_agent.heartbeat-testnamespace123", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, +} +`; + +exports[`getMonitoringPermissions Without elastic agent package installed should return default metrics permissions if only metrics are enabled 1`] = ` +Object { + "_elastic_agent_monitoring": Object { + "indices": Array [ + Object { + "names": Array [ + "metrics-elastic_agent-testnamespace123", + "metrics-elastic_agent.elastic_agent-testnamespace123", + "metrics-elastic_agent.apm_server-testnamespace123", + "metrics-elastic_agent.filebeat-testnamespace123", + "metrics-elastic_agent.fleet_server-testnamespace123", + "metrics-elastic_agent.metricbeat-testnamespace123", + "metrics-elastic_agent.osquerybeat-testnamespace123", + "metrics-elastic_agent.packetbeat-testnamespace123", + "metrics-elastic_agent.endpoint_security-testnamespace123", + "metrics-elastic_agent.auditbeat-testnamespace123", + "metrics-elastic_agent.heartbeat-testnamespace123", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, +} +`; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts index 8df1234982ee..9a9b200d1413 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts @@ -13,7 +13,11 @@ import { agentPolicyService } from '../agent_policy'; import { agentPolicyUpdateEventHandler } from '../agent_policy_update'; import { getFullAgentPolicy } from './full_agent_policy'; +import { getMonitoringPermissions } from './monitoring_permissions'; +const mockedGetElasticAgentMonitoringPermissions = getMonitoringPermissions as jest.Mock< + ReturnType +>; const mockedAgentPolicyService = agentPolicyService as jest.Mocked; function mockAgentPolicy(data: Partial) { @@ -87,6 +91,8 @@ jest.mock('../agent_policy_update'); jest.mock('../agents'); jest.mock('../package_policy'); +jest.mock('./monitoring_permissions'); + function getAgentPolicyUpdateMock() { return agentPolicyUpdateEventHandler as unknown as jest.Mock< typeof agentPolicyUpdateEventHandler @@ -97,6 +103,29 @@ describe('getFullAgentPolicy', () => { beforeEach(() => { getAgentPolicyUpdateMock().mockClear(); mockedAgentPolicyService.get.mockReset(); + mockedGetElasticAgentMonitoringPermissions.mockReset(); + mockedGetElasticAgentMonitoringPermissions.mockImplementation( + async (soClient, { logs, metrics }, namespace) => { + const names: string[] = []; + if (logs) { + names.push(`logs-${namespace}`); + } + if (metrics) { + names.push(`metrics-${namespace}`); + } + + return { + _elastic_agent_monitoring: { + indices: [ + { + names, + privileges: [], + }, + ], + }, + }; + } + ); }); it('should return a policy without monitoring if monitoring is not enabled', async () => { @@ -200,6 +229,24 @@ describe('getFullAgentPolicy', () => { }); }); + it('should get the permissions for monitoring', async () => { + mockAgentPolicy({ + namespace: 'testnamespace', + revision: 1, + monitoring_enabled: ['metrics'], + }); + await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy'); + + expect(mockedGetElasticAgentMonitoringPermissions).toHaveBeenCalledWith( + expect.anything(), + { + logs: false, + metrics: true, + }, + 'testnamespace' + ); + }); + it('should support a different monitoring output', async () => { mockAgentPolicy({ namespace: 'default', diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts index 4e8b3a2c1952..561c463b998d 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts @@ -24,21 +24,9 @@ import { import { storedPackagePoliciesToAgentInputs, dataTypes, outputType } from '../../../common'; import type { FullAgentPolicyOutputPermissions } from '../../../common'; import { getSettings } from '../settings'; -import { PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, DEFAULT_OUTPUT } from '../../constants'; - -const MONITORING_DATASETS = [ - 'elastic_agent', - 'elastic_agent.elastic_agent', - 'elastic_agent.apm_server', - 'elastic_agent.filebeat', - 'elastic_agent.fleet_server', - 'elastic_agent.metricbeat', - 'elastic_agent.osquerybeat', - 'elastic_agent.packetbeat', - 'elastic_agent.endpoint_security', - 'elastic_agent.auditbeat', - 'elastic_agent.heartbeat', -]; +import { DEFAULT_OUTPUT } from '../../constants'; + +import { getMonitoringPermissions } from './monitoring_permissions'; export async function getFullAgentPolicy( soClient: SavedObjectsClientContract, @@ -125,41 +113,17 @@ export async function getFullAgentPolicy( cluster: DEFAULT_PERMISSIONS.cluster, }; - // TODO: fetch this from the elastic agent package https://github.com/elastic/kibana/issues/107738 - const monitoringNamespace = fullAgentPolicy.agent?.monitoring.namespace; - const monitoringPermissions: FullAgentPolicyOutputPermissions = - monitoringOutputId === dataOutputId - ? dataPermissions - : { - _elastic_agent_checks: { - cluster: DEFAULT_PERMISSIONS.cluster, - }, - }; - if ( - fullAgentPolicy.agent?.monitoring.enabled && - monitoringNamespace && - monitoringOutput && - monitoringOutput.type === outputType.Elasticsearch - ) { - let names: string[] = []; - if (fullAgentPolicy.agent.monitoring.logs) { - names = names.concat( - MONITORING_DATASETS.map((dataset) => `logs-${dataset}-${monitoringNamespace}`) - ); - } - if (fullAgentPolicy.agent.monitoring.metrics) { - names = names.concat( - MONITORING_DATASETS.map((dataset) => `metrics-${dataset}-${monitoringNamespace}`) - ); - } - - monitoringPermissions._elastic_agent_checks.indices = [ - { - names, - privileges: PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, - }, - ]; - } + const monitoringPermissions = await getMonitoringPermissions( + soClient, + { + logs: agentPolicy.monitoring_enabled?.includes(dataTypes.Logs) ?? false, + metrics: agentPolicy.monitoring_enabled?.includes(dataTypes.Metrics) ?? false, + }, + agentPolicy.namespace + ); + monitoringPermissions._elastic_agent_checks = { + cluster: DEFAULT_PERMISSIONS.cluster, + }; // Only add permissions if output.type is "elasticsearch" fullAgentPolicy.output_permissions = Object.keys(fullAgentPolicy.outputs).reduce< @@ -167,10 +131,16 @@ export async function getFullAgentPolicy( >((outputPermissions, outputId) => { const output = fullAgentPolicy.outputs[outputId]; if (output && output.type === outputType.Elasticsearch) { - outputPermissions[outputId] = - outputId === getOutputIdForAgentPolicy(dataOutput) - ? dataPermissions - : monitoringPermissions; + const permissions: FullAgentPolicyOutputPermissions = {}; + if (outputId === getOutputIdForAgentPolicy(monitoringOutput)) { + Object.assign(permissions, monitoringPermissions); + } + + if (outputId === getOutputIdForAgentPolicy(dataOutput)) { + Object.assign(permissions, dataPermissions); + } + + outputPermissions[outputId] = permissions; } return outputPermissions; }, {}); diff --git a/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.test.ts new file mode 100644 index 000000000000..3d928bed0f66 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +import type { Installation, PackageInfo } from '../../types'; +import { getPackageInfo, getInstallation } from '../epm/packages'; + +import { getMonitoringPermissions } from './monitoring_permissions'; + +jest.mock('../epm/packages'); + +const mockedGetInstallation = getInstallation as jest.Mock>; +const mockedGetPackageInfo = getPackageInfo as jest.Mock>; + +describe('getMonitoringPermissions', () => { + describe('Without elastic agent package installed', () => { + it('should return default logs and metrics permissions if both are enabled', async () => { + const permissions = await getMonitoringPermissions( + savedObjectsClientMock.create(), + { logs: true, metrics: true }, + 'testnamespace123' + ); + expect(permissions).toMatchSnapshot(); + }); + it('should return default logs permissions if only logs are enabled', async () => { + const permissions = await getMonitoringPermissions( + savedObjectsClientMock.create(), + { logs: true, metrics: false }, + 'testnamespace123' + ); + expect(permissions).toMatchSnapshot(); + }); + it('should return default metrics permissions if only metrics are enabled', async () => { + const permissions = await getMonitoringPermissions( + savedObjectsClientMock.create(), + { logs: false, metrics: true }, + 'testnamespace123' + ); + expect(permissions).toMatchSnapshot(); + }); + }); + + describe('With elastic agent package installed', () => { + beforeEach(() => { + // Mock a simplified elastic agent package with only 4 datastreams logs and metrics for filebeat and metricbeat + mockedGetInstallation.mockResolvedValue({ + name: 'elastic_agent', + version: '1.0.0', + } as Installation); + mockedGetPackageInfo.mockResolvedValue({ + data_streams: [ + { + type: 'logs', + dataset: 'elastic_agent.metricbeat', + }, + { + type: 'metrics', + dataset: 'elastic_agent.metricbeat', + }, + { + type: 'logs', + dataset: 'elastic_agent.filebeat', + }, + { + type: 'metrics', + dataset: 'elastic_agent.filebeat', + }, + ], + } as PackageInfo); + }); + it('should return default logs and metrics permissions if both are enabled', async () => { + const permissions = await getMonitoringPermissions( + savedObjectsClientMock.create(), + { logs: true, metrics: true }, + 'testnamespace123' + ); + expect(permissions).toMatchSnapshot(); + }); + it('should return default logs permissions if only logs are enabled', async () => { + const permissions = await getMonitoringPermissions( + savedObjectsClientMock.create(), + { logs: true, metrics: false }, + 'testnamespace123' + ); + expect(permissions).toMatchSnapshot(); + }); + it('should return default metrics permissions if only metrics are enabled', async () => { + const permissions = await getMonitoringPermissions( + savedObjectsClientMock.create(), + { logs: false, metrics: true }, + 'testnamespace123' + ); + expect(permissions).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.ts b/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.ts new file mode 100644 index 000000000000..3533d829e134 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from 'kibana/server'; + +import { getPackageInfo, getInstallation } from '../epm/packages'; +import { getDataStreamPrivileges } from '../package_policies_to_agent_permissions'; +import { + PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, + AGENT_POLICY_DEFAULT_MONITORING_DATASETS, +} from '../../constants'; +import type { FullAgentPolicyOutputPermissions } from '../../../common'; +import { FLEET_ELASTIC_AGENT_PACKAGE } from '../../../common'; +import { dataTypes } from '../../../common'; + +function buildDefault(enabled: { logs: boolean; metrics: boolean }, namespace: string) { + let names: string[] = []; + if (enabled.logs) { + names = names.concat( + AGENT_POLICY_DEFAULT_MONITORING_DATASETS.map((dataset) => `logs-${dataset}-${namespace}`) + ); + } + if (enabled.metrics) { + names = names.concat( + AGENT_POLICY_DEFAULT_MONITORING_DATASETS.map((dataset) => `metrics-${dataset}-${namespace}`) + ); + } + + return { + _elastic_agent_monitoring: { + indices: [ + { + names, + privileges: PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, + }, + ], + }, + }; +} + +export async function getMonitoringPermissions( + soClient: SavedObjectsClientContract, + enabled: { logs: boolean; metrics: boolean }, + namespace: string +): Promise { + const installation = await getInstallation({ + savedObjectsClient: soClient, + pkgName: FLEET_ELASTIC_AGENT_PACKAGE, + }); + + if (!installation) { + return buildDefault(enabled, namespace); + } + + const pkg = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: installation.name, + pkgVersion: installation.version, + }); + + if (!pkg.data_streams || pkg.data_streams.length === 0) { + return buildDefault(enabled, namespace); + } + + return { + _elastic_agent_monitoring: { + indices: pkg.data_streams + .map((ds) => { + if (ds.type === dataTypes.Logs && !enabled.logs) { + return; + } + if (ds.type === dataTypes.Metrics && !enabled.metrics) { + return; + } + return getDataStreamPrivileges(ds, namespace); + }) + .filter( + ( + i + ): i is { + names: string[]; + privileges: string[]; + } => typeof i !== 'undefined' + ), + }, + }; +} diff --git a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts index 568aafddecba..dfca8511fd84 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { appContextService, licenseService } from '../../'; +import { appContextService } from '../../'; // from https://github.com/elastic/package-registry#docker (maybe from OpenAPI one day) // the unused variables cause a TS warning about unused values @@ -32,16 +32,9 @@ const getDefaultRegistryUrl = (): string => { export const getRegistryUrl = (): string => { const customUrl = appContextService.getConfig()?.registryUrl; - const isEnterprise = licenseService.isEnterprise(); - - if (customUrl && isEnterprise) { - return customUrl; - } if (customUrl) { - appContextService - .getLogger() - .warn('Enterprise license is required to use a custom registry url.'); + return customUrl; } return getDefaultRegistryUrl(); diff --git a/x-pack/plugins/fleet/server/services/fleet_server/index.ts b/x-pack/plugins/fleet/server/services/fleet_server/index.ts index 733d962a86e9..0d386b9ba499 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/index.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/index.ts @@ -50,22 +50,25 @@ export async function startFleetServerSetup() { _onResolve = resolve; }); const logger = appContextService.getLogger(); + + // Check for security if (!appContextService.hasSecurity()) { // Fleet will not work if security is not enabled logger?.warn('Fleet requires the security plugin to be enabled.'); return; } + // Log information about custom registry URL + const customUrl = appContextService.getConfig()?.registryUrl; + if (customUrl) { + logger.info( + `Custom registry url is an experimental feature and is unsupported. Using custom registry at ${customUrl}` + ); + } + try { // We need licence to be initialized before using the SO service. await licenseService.getLicenseInformation$()?.pipe(first())?.toPromise(); - - const customUrl = appContextService.getConfig()?.registryUrl; - const isEnterprise = licenseService.isEnterprise(); - if (customUrl && isEnterprise) { - logger.info('Custom registry url is an experimental feature and is unsupported.'); - } - await runFleetServerMigration(); _isFleetServerSetup = true; } catch (err) { diff --git a/x-pack/plugins/fleet/storybook/context/index.tsx b/x-pack/plugins/fleet/storybook/context/index.tsx index cf28d4ff5401..6c6b2e479d23 100644 --- a/x-pack/plugins/fleet/storybook/context/index.tsx +++ b/x-pack/plugins/fleet/storybook/context/index.tsx @@ -19,6 +19,8 @@ import type { FleetConfigType, FleetStartServices } from '../../public/plugin'; // TODO: This is a contract leak, and should be on the context, rather than a setter. import { setHttpClient } from '../../public/hooks/use_request'; +import { setCustomIntegrations } from '../../public/services/custom_integrations'; + import { getApplication } from './application'; import { getChrome } from './chrome'; import { getHttp } from './http'; @@ -58,6 +60,10 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({ }; setHttpClient(startServices.http); + setCustomIntegrations({ + getAppendCustomIntegrations: async () => [], + getReplacementCustomIntegrations: async () => [], + }); const config = { enabled: true, diff --git a/x-pack/plugins/fleet/storybook/smoke.test.tsx b/x-pack/plugins/fleet/storybook/smoke.test.tsx new file mode 100644 index 000000000000..e7bdc2e9d82a --- /dev/null +++ b/x-pack/plugins/fleet/storybook/smoke.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import { createElement } from 'react'; +import { act } from 'react-dom/test-utils'; +import initStoryshots from '@storybook/addon-storyshots'; + +initStoryshots({ + configPath: __dirname, + framework: 'react', + test: async ({ story }) => { + const renderer = mount(createElement(story.render)); + // wait until the element will perform all renders and resolve all promises (lazy loading, especially) + await act(() => new Promise((resolve) => setTimeout(resolve, 0))); + expect(renderer.html()).not.toContain('euiErrorBoundary'); + }, +}); diff --git a/x-pack/plugins/infra/common/infra_ml/infra_ml.ts b/x-pack/plugins/infra/common/infra_ml/infra_ml.ts index 18c9f591395e..81e1009a8058 100644 --- a/x-pack/plugins/infra/common/infra_ml/infra_ml.ts +++ b/x-pack/plugins/infra/common/infra_ml/infra_ml.ts @@ -12,6 +12,7 @@ export type JobStatus = | 'initializing' | 'stopped' | 'started' + | 'starting' | 'finished' | 'failed'; @@ -35,10 +36,10 @@ export type SetupStatus = * before this state was reached. */ export const isJobStatusWithResults = (jobStatus: JobStatus) => - ['started', 'finished', 'stopped', 'failed'].includes(jobStatus); + ['started', 'starting', 'finished', 'stopped', 'failed'].includes(jobStatus); export const isHealthyJobStatus = (jobStatus: JobStatus) => - ['started', 'finished'].includes(jobStatus); + ['started', 'starting', 'finished'].includes(jobStatus); /** * Maps a setup status to the possibility that results have already been diff --git a/x-pack/plugins/infra/common/log_analysis/log_analysis.ts b/x-pack/plugins/infra/common/log_analysis/log_analysis.ts index 18c9f591395e..81e1009a8058 100644 --- a/x-pack/plugins/infra/common/log_analysis/log_analysis.ts +++ b/x-pack/plugins/infra/common/log_analysis/log_analysis.ts @@ -12,6 +12,7 @@ export type JobStatus = | 'initializing' | 'stopped' | 'started' + | 'starting' | 'finished' | 'failed'; @@ -35,10 +36,10 @@ export type SetupStatus = * before this state was reached. */ export const isJobStatusWithResults = (jobStatus: JobStatus) => - ['started', 'finished', 'stopped', 'failed'].includes(jobStatus); + ['started', 'starting', 'finished', 'stopped', 'failed'].includes(jobStatus); export const isHealthyJobStatus = (jobStatus: JobStatus) => - ['started', 'finished'].includes(jobStatus); + ['started', 'starting', 'finished'].includes(jobStatus); /** * Maps a setup status to the possibility that results have already been diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts index d4e1f7366dd2..2a5f68b3c32e 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts @@ -41,6 +41,7 @@ export type FetchJobStatusRequestPayload = rt.TypeOf ); const nextSetupStatus: SetupStatus = Object.values(nextJobStatus).every( - (jobState) => jobState === 'started' + (jobState) => jobState === 'started' || jobState === 'starting' ) ? { type: 'succeeded' } : { @@ -224,9 +224,17 @@ const getJobStatus = jobSummary.datafeedState === 'stopped' ) { return 'stopped'; - } else if (jobSummary.jobState === 'opening') { + } else if ( + jobSummary.jobState === 'opening' && + jobSummary.awaitingNodeAssignment === false + ) { return 'initializing'; - } else if (jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') { + } else if ( + (jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') || + (jobSummary.jobState === 'opening' && + jobSummary.datafeedState === 'starting' && + jobSummary.awaitingNodeAssignment === true) + ) { return 'started'; } diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts index 585aa8786286..c7dc1d509cf1 100644 --- a/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts +++ b/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts @@ -41,6 +41,7 @@ export type FetchJobStatusRequestPayload = rt.TypeOf + + { 0} - hasK8sJobs={k8sJobSummaries.length > 0} - jobIds={jobIds} - /> - ) + <> + {tab === 'jobs' && hasJobs && ( + <> + 0} + hasK8sJobs={k8sJobSummaries.length > 0} + jobIds={jobIds} + /> + + )} + + } > {tab === 'jobs' && ( diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts index 28f9b08eff1c..4d53f96c71fd 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts @@ -7,11 +7,10 @@ import { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; import { functionWrapper } from 'src/plugins/expressions/common/expression_functions/specs/tests/utils'; -import { FormatColumnArgs, formatColumn } from './index'; +import { formatColumn } from './index'; describe('format_column', () => { - const fn: (input: Datatable, args: FormatColumnArgs) => Promise = - functionWrapper(formatColumn); + const fn = functionWrapper(formatColumn); let datatable: Datatable; diff --git a/x-pack/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 index 7f1e4aa58dba..f3245759c9ef 100644 --- a/x-pack/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 @@ -115,6 +115,7 @@ export function getSuggestions({ } else { dataSourceSuggestions = datasource.getDatasourceSuggestionsFromCurrentState( datasourceState, + (layerId) => isLayerSupportedByVisualization(layerId, [layerTypes.DATA]), activeData ); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index a5d6db4be331..bf4b10de386a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -1704,6 +1704,103 @@ describe('IndexPattern Data Source suggestions', () => { ); }); + it('adds date histogram over default time field for tables without time dimension and a threshold', async () => { + const initialState = testInitialState(); + const state: IndexPatternPrivateState = { + ...initialState, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['cola', 'colb'], + columns: { + cola: { + label: 'My Terms', + customLabel: true, + dataType: 'string', + isBucketed: true, + operationType: 'terms', + sourceField: 'source', + scale: 'ordinal', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 5, + }, + }, + colb: { + label: 'My Op', + customLabel: true, + dataType: 'number', + isBucketed: false, + operationType: 'average', + sourceField: 'bytes', + scale: 'ratio', + }, + }, + }, + threshold: { + indexPatternId: '2', + columnOrder: ['thresholda'], + columns: { + thresholda: { + label: 'My Op', + customLabel: true, + dataType: 'number', + isBucketed: false, + operationType: 'average', + sourceField: 'bytes', + scale: 'ratio', + }, + }, + }, + }, + }; + + expect( + getSuggestionSubset( + getDatasourceSuggestionsFromCurrentState(state, (layerId) => layerId !== 'threshold') + ) + ).toContainEqual( + expect.objectContaining({ + table: { + isMultiRow: true, + changeType: 'extended', + label: 'Over time', + columns: [ + { + columnId: 'cola', + operation: { + label: 'My Terms', + dataType: 'string', + isBucketed: true, + scale: 'ordinal', + }, + }, + { + columnId: 'id1', + operation: { + label: 'timestampLabel', + dataType: 'date', + isBucketed: true, + scale: 'interval', + }, + }, + { + columnId: 'colb', + operation: { + label: 'My Op', + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }, + }, + ], + layerId: 'first', + }, + }) + ); + }); + it('does not create an over time suggestion if tables with numeric buckets with time dimension', async () => { const initialState = testInitialState(); const state: IndexPatternPrivateState = { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 0fe0ef617dc2..604b63aa2924 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -350,9 +350,11 @@ function createNewLayerWithMetricAggregation( } export function getDatasourceSuggestionsFromCurrentState( - state: IndexPatternPrivateState + state: IndexPatternPrivateState, + filterLayers: (layerId: string) => boolean = () => true ): Array> { - const layers = Object.entries(state.layers || {}); + const layers = Object.entries(state.layers || {}).filter(([layerId]) => filterLayers(layerId)); + if (layers.length > 1) { // Return suggestions that reduce the data to each layer individually return layers @@ -394,7 +396,7 @@ export function getDatasourceSuggestionsFromCurrentState( } return flatten( - Object.entries(state.layers || {}) + layers .filter(([_id, layer]) => layer.columnOrder.length && layer.indexPatternId) .map(([layerId, layer]) => { const indexPattern = state.indexPatterns[layer.indexPatternId]; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 75ed5f4907e0..2e7876f83fc4 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -246,6 +246,7 @@ export interface Datasource { ) => Array>; getDatasourceSuggestionsFromCurrentState: ( state: T, + filterFn?: (layerId: string) => boolean, activeData?: Record ) => Array>; diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index 2a4941995054..d174fb831c2d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -13,6 +13,7 @@ import { Operation } from '../types'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; import { layerTypes } from '../../common'; import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; +import { defaultThresholdColor } from './color_assignment'; describe('#toExpression', () => { const xyVisualization = getXyVisualization({ @@ -319,4 +320,42 @@ describe('#toExpression', () => { ) as Ast; expect(expression.chain[0].arguments.valueLabels[0] as Ast).toEqual('inside'); }); + + it('should compute the correct series color fallback based on the layer type', () => { + const expression = xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + valueLabels: 'inside', + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + layerType: layerTypes.DATA, + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + yConfig: [{ forAccessor: 'a' }], + }, + { + layerId: 'threshold', + layerType: layerTypes.THRESHOLD, + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + yConfig: [{ forAccessor: 'a' }], + }, + ], + }, + { ...frame.datasourceLayers, threshold: mockDatasource.publicAPIMock } + ) as Ast; + + function getYConfigColorForLayer(ast: Ast, index: number) { + return ((ast.chain[0].arguments.layers[index] as Ast).chain[0].arguments.yConfig[0] as Ast) + .chain[0].arguments.color; + } + expect(getYConfigColorForLayer(expression, 0)).toEqual([]); + expect(getYConfigColorForLayer(expression, 1)).toEqual([defaultThresholdColor]); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 2fce7c6a612a..bb65b69a8d12 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -335,7 +335,12 @@ export const buildExpression = ( arguments: { forAccessor: [yConfig.forAccessor], axisMode: yConfig.axisMode ? [yConfig.axisMode] : [], - color: [yConfig.color || defaultThresholdColor], + color: + layer.layerType === layerTypes.THRESHOLD + ? [yConfig.color || defaultThresholdColor] + : yConfig.color + ? [yConfig.color] + : [], lineStyle: [yConfig.lineStyle || 'solid'], lineWidth: [yConfig.lineWidth || 1], fill: [yConfig.fill || 'none'], diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/index.ts b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/index.ts index a0a79c10f3ef..fd87a7e816f6 100644 --- a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/index.ts +++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/index.ts @@ -7,3 +7,4 @@ export { JobsAwaitingNodeWarning } from './jobs_awaiting_node_warning'; export { NewJobAwaitingNodeWarning } from './new_job_awaiting_node'; +export { MLJobsAwaitingNodeWarning } from './new_job_awaiting_node_shared'; diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx index 2cc36b7a2adf..2f51fd6e32a6 100644 --- a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx +++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, FC } from 'react'; +import React, { FC } from 'react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -21,7 +21,7 @@ export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => { } return ( - + <> = ({ jobCount }) => { - + ); }; diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx index ce31d1afc475..f181497a9efc 100644 --- a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx +++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, FC } from 'react'; +import React, { FC } from 'react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -22,7 +22,7 @@ export const NewJobAwaitingNodeWarning: FC = () => { } return ( - + <> = () => { - + ); }; diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/index.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/index.tsx new file mode 100644 index 000000000000..0457e7dd18d3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { MLJobsAwaitingNodeWarning } from './lazy_loader'; diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/lazy_loader.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/lazy_loader.tsx new file mode 100644 index 000000000000..655bde61ccc5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/lazy_loader.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; + +const MLJobsAwaitingNodeWarningComponent = React.lazy( + () => import('./new_job_awaiting_node_shared') +); + +interface Props { + jobIds: string[]; +} + +export const MLJobsAwaitingNodeWarning: FC = ({ jobIds }) => { + return ( + }> + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/new_job_awaiting_node_shared.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/new_job_awaiting_node_shared.tsx new file mode 100644 index 000000000000..5850349ff5fd --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/new_job_awaiting_node_shared.tsx @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useState, useEffect, useCallback, useMemo } from 'react'; +import { estypes } from '@elastic/elasticsearch'; + +import { EuiCallOut, EuiSpacer, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { JOB_STATE } from '../../../../../common/constants/states'; +import { mlApiServicesProvider } from '../../../services/ml_api_service'; +import { HttpService } from '../../../services/http_service'; +import { extractDeploymentId, CloudInfo } from '../../../services/ml_server_info'; + +interface Props { + jobIds: string[]; +} + +function isJobAwaitingNodeAssignment(job: estypes.MlJobStats) { + return job.node === undefined && job.state === JOB_STATE.OPENING; +} + +const MLJobsAwaitingNodeWarning: FC = ({ jobIds }) => { + const { http } = useKibana().services; + const ml = useMemo(() => mlApiServicesProvider(new HttpService(http!)), [http]); + + const [unassignedJobCount, setUnassignedJobCount] = useState(0); + const [cloudInfo, setCloudInfo] = useState(null); + + const checkNodes = useCallback(async () => { + try { + if (jobIds.length === 0) { + setUnassignedJobCount(0); + return; + } + + const { lazyNodeCount } = await ml.mlNodeCount(); + if (lazyNodeCount === 0) { + setUnassignedJobCount(0); + return; + } + + const { jobs } = await ml.getJobStats({ jobId: jobIds.join(',') }); + const unassignedJobs = jobs.filter(isJobAwaitingNodeAssignment); + setUnassignedJobCount(unassignedJobs.length); + } catch (error) { + setUnassignedJobCount(0); + // eslint-disable-next-line no-console + console.error('Could not determine ML node information', error); + } + }, [jobIds]); + + const checkCloudInfo = useCallback(async () => { + if (unassignedJobCount === 0) { + return; + } + + try { + const resp = await ml.mlInfo(); + const cloudId = resp.cloudId ?? null; + setCloudInfo({ + isCloud: cloudId !== null, + cloudId, + deploymentId: cloudId === null ? null : extractDeploymentId(cloudId), + }); + } catch (error) { + setCloudInfo(null); + // eslint-disable-next-line no-console + console.error('Could not determine cloud information', error); + } + }, [unassignedJobCount]); + + useEffect(() => { + checkCloudInfo(); + }, [unassignedJobCount]); + + useEffect(() => { + checkNodes(); + }, [jobIds]); + + if (unassignedJobCount === 0) { + return null; + } + + return ( + <> + + } + color="primary" + iconType="iInCircle" + > +
+ + + {cloudInfo && + (cloudInfo.isCloud ? ( + <> + + {cloudInfo.deploymentId === null ? null : ( + + + + ), + }} + /> + )} + + ) : ( + + + + ), + }} + /> + ))} +
+
+ + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default MLJobsAwaitingNodeWarning; diff --git a/x-pack/plugins/ml/public/application/services/ml_server_info.ts b/x-pack/plugins/ml/public/application/services/ml_server_info.ts index 21a1773206c4..c13ac9433749 100644 --- a/x-pack/plugins/ml/public/application/services/ml_server_info.ts +++ b/x-pack/plugins/ml/public/application/services/ml_server_info.ts @@ -11,6 +11,7 @@ import { MlServerDefaults, MlServerLimits } from '../../../common/types/ml_serve export interface CloudInfo { cloudId: string | null; isCloud: boolean; + deploymentId: string | null; } let defaults: MlServerDefaults = { @@ -22,6 +23,7 @@ let limits: MlServerLimits = {}; const cloudInfo: CloudInfo = { cloudId: null, isCloud: false, + deploymentId: null, }; export async function loadMlServerInfo() { @@ -31,6 +33,7 @@ export async function loadMlServerInfo() { limits = resp.limits; cloudInfo.cloudId = resp.cloudId || null; cloudInfo.isCloud = resp.cloudId !== undefined; + cloudInfo.deploymentId = !resp.cloudId ? null : extractDeploymentId(resp.cloudId); return { defaults, limits, cloudId: cloudInfo }; } catch (error) { return { defaults, limits, cloudId: cloudInfo }; @@ -54,7 +57,7 @@ export function isCloud(): boolean { } export function getCloudDeploymentId(): string | null { - return cloudInfo.cloudId === null ? null : extractDeploymentId(cloudInfo.cloudId); + return cloudInfo.deploymentId; } export function extractDeploymentId(cloudId: string) { diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index 78090c611b47..6af8b8a6c876 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -64,3 +64,5 @@ export const getMlSharedImports = async () => { // Helper to get Type returned by getMlSharedImports. type AwaitReturnType = T extends PromiseLike ? U : T; export type GetMlSharedImportsReturnType = AwaitReturnType>; + +export { MLJobsAwaitingNodeWarning } from './application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared'; diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index dea8d18bb65b..09698b4bb56e 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -23,12 +23,23 @@ import { ElasticsearchOverviewPage } from './pages/elasticsearch/overview'; import { BeatsOverviewPage } from './pages/beats/overview'; import { BeatsInstancesPage } from './pages/beats/instances'; import { BeatsInstancePage } from './pages/beats/instance'; -import { CODE_PATH_ELASTICSEARCH, CODE_PATH_BEATS } from '../../common/constants'; +import { ApmOverviewPage, ApmInstancesPage, ApmInstancePage } from './pages/apm'; +import { KibanaOverviewPage } from './pages/kibana/overview'; +import { KibanaInstancesPage } from './pages/kibana/instances'; import { ElasticsearchNodesPage } from './pages/elasticsearch/nodes_page'; import { ElasticsearchIndicesPage } from './pages/elasticsearch/indices_page'; +import { ElasticsearchIndexPage } from './pages/elasticsearch/index_page'; +import { ElasticsearchIndexAdvancedPage } from './pages/elasticsearch/index_advanced_page'; import { ElasticsearchNodePage } from './pages/elasticsearch/node_page'; +import { ElasticsearchNodeAdvancedPage } from './pages/elasticsearch/node_advanced_page'; import { MonitoringTimeContainer } from './hooks/use_monitoring_time'; import { BreadcrumbContainer } from './hooks/use_breadcrumbs'; +import { + CODE_PATH_ELASTICSEARCH, + CODE_PATH_KIBANA, + CODE_PATH_BEATS, + CODE_PATH_APM, +} from '../../common/constants'; export const renderApp = ( core: CoreStart, @@ -83,6 +94,20 @@ const MonitoringApp: React.FC<{ /> {/* ElasticSearch Views */} + + + + + + + {/* Kibana Views */} + + + + {/* Beats Views */} + {/* APM Views */} + + + + + + = ({ ...props }) => { + const tabs: TabMenuItem[] = [ + { + id: 'overview', + label: i18n.translate('xpack.monitoring.apmNavigation.overviewLinkText', { + defaultMessage: 'Overview', + }), + route: '/apm', + }, + { + id: 'instances', + label: i18n.translate('xpack.monitoring.apmNavigation.instancesLinkText', { + defaultMessage: 'Instances', + }), + route: '/apm/instances', + }, + ]; + + return ; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/index.ts b/x-pack/plugins/monitoring/public/application/pages/apm/index.ts new file mode 100644 index 000000000000..209e293c4935 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/apm/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ApmOverviewPage } from './overview'; +export { ApmInstancesPage } from './instances'; +export { ApmInstancePage } from './instance'; diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/instance.tsx b/x-pack/plugins/monitoring/public/application/pages/apm/instance.tsx new file mode 100644 index 000000000000..dc55ecb22b61 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/apm/instance.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useState, useCallback, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ComponentProps } from '../../route_init'; +import { GlobalStateContext } from '../../global_state_context'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useCharts } from '../../hooks/use_charts'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; +import { PageTemplate } from '../page_template'; +// @ts-ignore +import { ApmServerInstance } from '../../../components/apm/instance'; + +export const ApmInstancePage: React.FC = ({ clusters }) => { + const { instance }: { instance: string } = useParams(); + + const globalState = useContext(GlobalStateContext); + const { services } = useKibana<{ data: any }>(); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + const { zoomInfo, onBrush } = useCharts(); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + const [data, setData] = useState({} as any); + const [instanceName, setInstanceName] = useState(''); + + const title = i18n.translate('xpack.monitoring.apm.instance.routeTitle', { + defaultMessage: '{apm} - Instance', + values: { + apm: 'APM server', + }, + }); + + const pageTitle = i18n.translate('xpack.monitoring.apm.instance.pageTitle', { + defaultMessage: 'APM server instance: {instanceName}', + values: { + instanceName, + }, + }); + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inApm: true, + instance: instanceName, + }); + } + }, [cluster, instanceName, generateBreadcrumbs]); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/apm/${instance}`; + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + setInstanceName(response.apmSummary.name); + }, [ccs, clusterUuid, instance, services.data?.query.timefilter.timefilter, services.http]); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx new file mode 100644 index 000000000000..86ea8079277c --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/apm/instances.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ComponentProps } from '../../route_init'; +import { GlobalStateContext } from '../../global_state_context'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useTable } from '../../hooks/use_table'; +import { ApmTemplate } from './apm_template'; +// @ts-ignore +import { ApmServerInstances } from '../../../components/apm/instances'; +import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; + +interface SetupModeProps { + setupMode: any; + flyoutComponent: any; + bottomBarComponent: any; +} + +export const ApmInstancesPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { services } = useKibana<{ data: any }>(); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + const { updateTotalItemCount, getPaginationTableProps } = useTable('apm.instances'); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + const [data, setData] = useState({} as any); + + const title = i18n.translate('xpack.monitoring.apm.instances.routeTitle', { + defaultMessage: '{apm} - Instances', + values: { + apm: 'APM server', + }, + }); + + const pageTitle = i18n.translate('xpack.monitoring.apm.instances.pageTitle', { + defaultMessage: 'APM server instances', + }); + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inApm: true, + }); + } + }, [cluster, generateBreadcrumbs]); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/apm/instances`; + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + updateTotalItemCount(response.stats.total); + }, [ + ccs, + clusterUuid, + services.data?.query.timefilter.timefilter, + services.http, + updateTotalItemCount, + ]); + + const { pagination, sorting, onTableChange } = getPaginationTableProps(); + + return ( + + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> + + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/apm/overview.tsx new file mode 100644 index 000000000000..cca31c0a7e65 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/apm/overview.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ComponentProps } from '../../route_init'; +import { ApmTemplate } from './apm_template'; +import { GlobalStateContext } from '../../global_state_context'; +import { useCharts } from '../../hooks/use_charts'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; +// @ts-ignore +import { ApmOverview } from '../../../components/apm/overview'; + +export const ApmOverviewPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { zoomInfo, onBrush } = useCharts(); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + + const [data, setData] = useState(null); + + const title = i18n.translate('xpack.monitoring.apm.overview.routeTitle', { + defaultMessage: 'APM server', + }); + + const pageTitle = i18n.translate('xpack.monitoring.apm.overview.pageTitle', { + defaultMessage: 'APM server overview', + }); + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inApm: true, + }); + } + }, [cluster, generateBreadcrumbs]); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/apm`; + + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + + return ( + + {data && } + + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx index 7a65022d8ff5..29945f0fe725 100644 --- a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx @@ -15,16 +15,10 @@ import { useTable } from '../../hooks/use_table'; import { BeatsTemplate } from './beats_template'; // @ts-ignore import { Listing } from '../../../components/beats/listing'; -import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; -interface SetupModeProps { - setupMode: any; - flyoutComponent: any; - bottomBarComponent: any; -} - export const BeatsInstancesPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); diff --git a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx index 9bf0bf6d1479..3ea965f9c3bf 100644 --- a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx @@ -14,17 +14,12 @@ import { GlobalStateContext } from '../../global_state_context'; import { TabMenuItem } from '../page_template'; import { Overview } from '../../../components/cluster/overview'; import { ExternalConfigContext } from '../../external_config_context'; -import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; const CODE_PATHS = [CODE_PATH_ALL]; -interface SetupModeProps { - setupMode: any; - flyoutComponent: any; - bottomBarComponent: any; -} export const ClusterOverview: React.FC<{}> = () => { const state = useContext(GlobalStateContext); diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx new file mode 100644 index 000000000000..a55e0b5df964 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useContext, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useParams } from 'react-router-dom'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ComponentProps } from '../../route_init'; +import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; +import { useCharts } from '../../hooks/use_charts'; +import { ItemTemplate } from './item_template'; +// @ts-ignore +import { AdvancedIndex } from '../../../components/elasticsearch/index/advanced'; + +export const ElasticsearchIndexAdvancedPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { services } = useKibana<{ data: any }>(); + const { index }: { index: string } = useParams(); + const { zoomInfo, onBrush } = useCharts(); + const clusterUuid = globalState.cluster_uuid; + const [data, setData] = useState({} as any); + + const title = i18n.translate('xpack.monitoring.elasticsearch.index.advanced.title', { + defaultMessage: 'Elasticsearch - Indices - {indexName} - Advanced', + values: { + indexName: index, + }, + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices/${index}`; + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: true, + }), + }); + setData(response); + }, [clusterUuid, services.data?.query.timefilter.timefilter, services.http, index]); + + return ( + + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> + + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx new file mode 100644 index 000000000000..4f659f6c1354 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useContext, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useParams } from 'react-router-dom'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +// @ts-ignore +import { IndexReact } from '../../../components/elasticsearch/index/index_react'; +import { ComponentProps } from '../../route_init'; +import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; +import { useCharts } from '../../hooks/use_charts'; +import { ItemTemplate } from './item_template'; +// @ts-ignore +import { indicesByNodes } from '../../../components/elasticsearch/shard_allocation/transformers/indices_by_nodes'; +// @ts-ignore +import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; + +export const ElasticsearchIndexPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { services } = useKibana<{ data: any }>(); + const { index }: { index: string } = useParams(); + const { zoomInfo, onBrush } = useCharts(); + const clusterUuid = globalState.cluster_uuid; + const [data, setData] = useState({} as any); + const [indexLabel, setIndexLabel] = useState(labels.index as any); + const [nodesByIndicesData, setNodesByIndicesData] = useState([]); + + const title = i18n.translate('xpack.monitoring.elasticsearch.index.overview.title', { + defaultMessage: 'Elasticsearch - Indices - {indexName} - Overview', + values: { + indexName: index, + }, + }); + + const pageTitle = i18n.translate('xpack.monitoring.elasticsearch.index.overview.pageTitle', { + defaultMessage: 'Index: {indexName}', + values: { + indexName: index, + }, + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices/${index}`; + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: false, + }), + }); + setData(response); + const transformer = indicesByNodes(); + setNodesByIndicesData(transformer(response.shards, response.nodes)); + + const shards = response.shards; + if (shards.some((shard: any) => shard.state === 'UNASSIGNED')) { + setIndexLabel(labels.indexWithUnassigned); + } + }, [clusterUuid, services.data?.query.timefilter.timefilter, services.http, index]); + + return ( + + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> + + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx index 9166f2090d89..8d5f7bfebc2b 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx @@ -12,17 +12,11 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' import { GlobalStateContext } from '../../global_state_context'; import { ElasticsearchIndices } from '../../../components/elasticsearch'; import { ComponentProps } from '../../route_init'; -import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useTable } from '../../hooks/use_table'; import { useLocalStorage } from '../../hooks/use_local_storage'; -interface SetupModeProps { - setupMode: any; - flyoutComponent: any; - bottomBarComponent: any; -} - export const ElasticsearchIndicesPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx new file mode 100644 index 000000000000..1f06ba18bf10 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { PageTemplate } from '../page_template'; +import { TabMenuItem, PageTemplateProps } from '../page_template'; + +interface ItemTemplateProps extends PageTemplateProps { + id: string; + pageType: string; +} +export const ItemTemplate: React.FC = (props) => { + const { pageType, id, ...rest } = props; + const tabs: TabMenuItem[] = [ + { + id: 'overview', + label: i18n.translate('xpack.monitoring.esItemNavigation.overviewLinkText', { + defaultMessage: 'Overview', + }), + route: `/elasticsearch/${pageType}/${id}`, + }, + { + id: 'advanced', + label: i18n.translate('xpack.monitoring.esItemNavigation.advancedLinkText', { + defaultMessage: 'Advanced', + }), + route: `/elasticsearch/${pageType}/${id}/advanced`, + }, + ]; + + return ; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_advanced_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_advanced_page.tsx new file mode 100644 index 000000000000..b35dab7ff58f --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_advanced_page.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useContext, useState, useCallback } from 'react'; +import { useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { ItemTemplate } from './item_template'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +// @ts-ignore +import { AdvancedNode } from '../../../components/elasticsearch/node/advanced'; +import { ComponentProps } from '../../route_init'; +import { useCharts } from '../../hooks/use_charts'; + +export const ElasticsearchNodeAdvancedPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { zoomInfo, onBrush } = useCharts(); + + const { node }: { node: string } = useParams(); + const { services } = useKibana<{ data: any }>(); + + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const [data, setData] = useState({} as any); + + const title = i18n.translate('xpack.monitoring.elasticsearch.node.advanced.title', { + defaultMessage: 'Elasticsearch - Nodes - {nodeName} - Advanced', + values: { + nodeName: data?.nodeSummary?.name, + }, + }); + + const pageTitle = i18n.translate('xpack.monitoring.elasticsearch.node.advanced.pageTitle', { + defaultMessage: 'Elasticsearch node: {nodeName}', + values: { + nodeName: data?.nodeSummary?.name, + }, + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes/${node}`; + + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: true, + }), + }); + + setData(response); + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http, node]); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx index ffbde2efcac6..58acd77afc62 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx @@ -7,23 +7,18 @@ import React, { useContext, useState, useCallback } from 'react'; import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; -import { ElasticsearchTemplate } from './elasticsearch_template'; +import { ItemTemplate } from './item_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { GlobalStateContext } from '../../global_state_context'; import { NodeReact } from '../../../components/elasticsearch'; import { ComponentProps } from '../../route_init'; -import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useLocalStorage } from '../../hooks/use_local_storage'; import { useCharts } from '../../hooks/use_charts'; import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices'; - -interface SetupModeProps { - setupMode: any; - flyoutComponent: any; - bottomBarComponent: any; -} +// @ts-ignore +import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; export const ElasticsearchNodePage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -38,13 +33,10 @@ export const ElasticsearchNodePage: React.FC = ({ clusters }) => const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; - const cluster = find(clusters, { - cluster_uuid: clusterUuid, - }); const [data, setData] = useState({} as any); const [nodesByIndicesData, setNodesByIndicesData] = useState([]); - const title = i18n.translate('xpack.monitoring.elasticsearch.node.overview.routeTitle', { + const title = i18n.translate('xpack.monitoring.elasticsearch.node.overview.title', { defaultMessage: 'Elasticsearch - Nodes - {nodeName} - Overview', values: { nodeName: data?.nodeSummary?.name, @@ -92,33 +84,33 @@ export const ElasticsearchNodePage: React.FC = ({ clusters }) => }, [showSystemIndices, setShowSystemIndices]); return ( - -
- ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> -
-
+ ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> + ); }; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx index 1fee700b4d92..d91b8b0441c5 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx @@ -13,17 +13,11 @@ import { GlobalStateContext } from '../../global_state_context'; import { ExternalConfigContext } from '../../external_config_context'; import { ElasticsearchNodes } from '../../../components/elasticsearch'; import { ComponentProps } from '../../route_init'; -import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useTable } from '../../hooks/use_table'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; -interface SetupModeProps { - setupMode: any; - flyoutComponent: any; - bottomBarComponent: any; -} - export const ElasticsearchNodesPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); const { showCgroupMetricsElasticsearch } = useContext(ExternalConfigContext); diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx new file mode 100644 index 000000000000..12f3214b7369 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ComponentProps } from '../../route_init'; +import { GlobalStateContext } from '../../global_state_context'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useTable } from '../../hooks/use_table'; +import { KibanaTemplate } from './kibana_template'; +// @ts-ignore +import { KibanaInstances } from '../../../components/kibana/instances'; +// @ts-ignore +import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; + +export const KibanaInstancesPage: React.FC = ({ clusters }) => { + const { cluster_uuid: clusterUuid, ccs } = useContext(GlobalStateContext); + const { services } = useKibana<{ data: any }>(); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + const { updateTotalItemCount, getPaginationTableProps } = useTable('kibana.instances'); + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + const [data, setData] = useState({} as any); + + const title = i18n.translate('xpack.monitoring.kibana.instances.routeTitle', { + defaultMessage: 'Kibana - Instances', + }); + + const pageTitle = i18n.translate('xpack.monitoring.kibana.instances.pageTitle', { + defaultMessage: 'Kibana instances', + }); + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inKibana: true, + }); + } + }, [cluster, generateBreadcrumbs]); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/kibana/instances`; + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + updateTotalItemCount(response.stats.total); + }, [ + ccs, + clusterUuid, + services.data?.query.timefilter.timefilter, + services.http, + updateTotalItemCount, + ]); + + return ( + +
+ ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> +
+
+ ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/kibana_template.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/kibana_template.tsx new file mode 100644 index 000000000000..56c763b726d2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/kibana_template.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { PageTemplate, TabMenuItem, PageTemplateProps } from '../page_template'; + +export const KibanaTemplate: React.FC = ({ ...props }) => { + const tabs: TabMenuItem[] = [ + { + id: 'overview', + label: i18n.translate('xpack.monitoring.kibanaNavigation.overviewLinkText', { + defaultMessage: 'Overview', + }), + route: '/kibana', + }, + { + id: 'instances', + label: i18n.translate('xpack.monitoring.kibanaNavigation.instancesLinkText', { + defaultMessage: 'Instances', + }), + route: '/kibana/instances', + }, + ]; + + return ; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx new file mode 100644 index 000000000000..2356011a3f77 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/overview.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPanel, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; + +import { KibanaTemplate } from './kibana_template'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ComponentProps } from '../../route_init'; +// @ts-ignore +import { MonitoringTimeseriesContainer } from '../../../components/chart'; +// @ts-ignore +import { ClusterStatus } from '../../../components/kibana/cluster_status'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; +import { useCharts } from '../../hooks/use_charts'; + +const KibanaOverview = ({ data }: { data: any }) => { + const { zoomInfo, onBrush } = useCharts(); + + if (!data) return null; + + return ( + + + + + + + + + + + + + + + + + + + ); +}; + +export const KibanaOverviewPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { services } = useKibana<{ data: any }>(); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + const [data, setData] = useState(); + const clusterUuid = globalState.cluster_uuid; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + const ccs = globalState.ccs; + const title = i18n.translate('xpack.monitoring.kibana.overview.title', { + defaultMessage: 'Kibana', + }); + const pageTitle = i18n.translate('xpack.monitoring.kibana.overview.pageTitle', { + defaultMessage: 'Kibana overview', + }); + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inKibana: true, + }); + } + }, [cluster, generateBreadcrumbs]); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/kibana`; + + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts index 27462f07c07b..48e8ee13059c 100644 --- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts @@ -6,3 +6,9 @@ */ export const SetupModeRenderer: FunctionComponent; + +export interface SetupModeProps { + setupMode: any; + flyoutComponent: any; + bottomBarComponent: any; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js new file mode 100644 index 000000000000..70bac52a0926 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiPage, + EuiPageContent, + EuiPageBody, + EuiPanel, + EuiSpacer, + EuiFlexGrid, + EuiFlexItem, +} from '@elastic/eui'; +import { IndexDetailStatus } from '../index_detail_status'; +import { MonitoringTimeseriesContainer } from '../../chart'; +import { ShardAllocationReact } from '../shard_allocation/shard_allocation_react'; +import { Logs } from '../../logs'; +import { AlertsCallout } from '../../../alerts/callout'; + +export const IndexReact = ({ + indexSummary, + metrics, + clusterUuid, + indexUuid, + logs, + alerts, + ...props +}) => { + const metricsToShow = [ + metrics.index_mem, + metrics.index_size, + metrics.index_search_request_rate, + metrics.index_request_rate, + metrics.index_segment_count, + metrics.index_document_count, + ]; + + return ( + + + + + + + + + + + {metricsToShow.map((metric, index) => ( + + + + + ))} + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js index 987ca467931f..2d0c4b59df4b 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js @@ -8,13 +8,12 @@ import React from 'react'; import { TableHeadReact } from './table_head_react'; import { TableBody } from './table_body'; -import { labels } from '../lib/labels'; export const ClusterViewReact = (props) => { return ( @@ -22,7 +21,7 @@ export const ClusterViewReact = (props) => { filter={props.filter} totalCount={props.totalCount} rows={props.nodesByIndices} - cols={labels.node.length} + cols={props.labels.length} shardStats={props.shardStats} />
diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx new file mode 100644 index 000000000000..0e17c6277618 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiHeaderLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibana } from '../../../utils/kibana_react'; + +export function MobileAddData() { + const kibana = useKibana(); + + return ( + + {ADD_DATA_LABEL} + + ); +} + +const ADD_DATA_LABEL = i18n.translate('xpack.observability.mobile.addDataButtonLabel', { + defaultMessage: 'Add Mobile data', +}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx new file mode 100644 index 000000000000..af91624769e6 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiHeaderLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibana } from '../../../utils/kibana_react'; + +export function SyntheticsAddData() { + const kibana = useKibana(); + + return ( + + {ADD_DATA_LABEL} + + ); +} + +const ADD_DATA_LABEL = i18n.translate('xpack.observability..synthetics.addDataButtonLabel', { + defaultMessage: 'Add synthetics data', +}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx new file mode 100644 index 000000000000..c6aa0742466f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiHeaderLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibana } from '../../../utils/kibana_react'; + +export function UXAddData() { + const kibana = useKibana(); + + return ( + + {ADD_DATA_LABEL} + + ); +} + +const ADD_DATA_LABEL = i18n.translate('xpack.observability.ux.addDataButtonLabel', { + defaultMessage: 'Add UX data', +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx new file mode 100644 index 000000000000..2b59628c3e8d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '../../rtl_helpers'; +import { fireEvent, screen } from '@testing-library/dom'; +import React from 'react'; +import { sampleAttribute } from '../../configurations/test_data/sample_attribute'; +import * as pluginHook from '../../../../../hooks/use_plugin_context'; +import { TypedLensByValueInput } from '../../../../../../../lens/public'; +import { ExpViewActionMenuContent } from './action_menu'; + +jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ + appMountParameters: { + setHeaderActionMenu: jest.fn(), + }, +} as any); + +describe('Action Menu', function () { + it('should be able to click open in lens', async function () { + const { findByText, core } = render( + + ); + + expect(await screen.findByText('Open in Lens')).toBeInTheDocument(); + + fireEvent.click(await findByText('Open in Lens')); + + expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); + expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( + { + id: '', + attributes: sampleAttribute, + timeRange: { to: 'now', from: 'now-10m' }, + }, + { + openInNewTab: true, + } + ); + }); + + it('should be able to click save', async function () { + const { findByText } = render( + + ); + + expect(await screen.findByText('Save')).toBeInTheDocument(); + + fireEvent.click(await findByText('Save')); + + expect(await screen.findByText('Lens Save Modal Component')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx new file mode 100644 index 000000000000..08b4a3b948c5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { LensEmbeddableInput, TypedLensByValueInput } from '../../../../../../../lens/public'; +import { ObservabilityAppServices } from '../../../../../application/types'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { AddToCaseAction } from '../../header/add_to_case_action'; + +export function ExpViewActionMenuContent({ + timeRange, + lensAttributes, +}: { + timeRange?: { from: string; to: string }; + lensAttributes: TypedLensByValueInput['attributes'] | null; +}) { + const kServices = useKibana().services; + + const { lens } = kServices; + + const [isSaveOpen, setIsSaveOpen] = useState(false); + + const LensSaveModalComponent = lens.SaveModalComponent; + + return ( + <> + + + + + + { + if (lensAttributes) { + lens.navigateToPrefilledEditor( + { + id: '', + timeRange, + attributes: lensAttributes, + }, + { + openInNewTab: true, + } + ); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.openInLens', { + defaultMessage: 'Open in Lens', + })} + + + + { + if (lensAttributes) { + setIsSaveOpen(true); + } + }} + size="s" + > + {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { + defaultMessage: 'Save', + })} + + + + + {isSaveOpen && lensAttributes && ( + setIsSaveOpen(false)} + // if we want to do anything after the viz is saved + // right now there is no action, so an empty function + onSave={() => {}} + /> + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx new file mode 100644 index 000000000000..23500b63e900 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ExpViewActionMenuContent } from './action_menu'; +import HeaderMenuPortal from '../../../header_menu_portal'; +import { usePluginContext } from '../../../../../hooks/use_plugin_context'; +import { TypedLensByValueInput } from '../../../../../../../lens/public'; + +interface Props { + timeRange?: { from: string; to: string }; + lensAttributes: TypedLensByValueInput['attributes'] | null; +} +export function ExpViewActionMenu(props: Props) { + const { appMountParameters } = usePluginContext(); + + return ( + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx similarity index 58% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx index c30863585b3b..aabde404aa7b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx @@ -6,48 +6,48 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; -import DateMath from '@elastic/datemath'; import { Moment } from 'moment'; +import DateMath from '@elastic/datemath'; +import { i18n } from '@kbn/i18n'; import { useSeriesStorage } from '../hooks/use_series_storage'; import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; +import { SeriesUrl } from '../types'; +import { ReportTypes } from '../configurations/constants'; export const parseAbsoluteDate = (date: string, options = {}) => { return DateMath.parse(date, options)!; }; -export function DateRangePicker({ seriesId }: { seriesId: string }) { - const { firstSeriesId, getSeries, setSeries } = useSeriesStorage(); +export function DateRangePicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) { + const { firstSeries, setSeries, reportType } = useSeriesStorage(); const dateFormat = useUiSetting('dateFormat'); - const { - time: { from, to }, - reportType, - } = getSeries(firstSeriesId); + const seriesFrom = series.time?.from; + const seriesTo = series.time?.to; - const series = getSeries(seriesId); + const { from: mainFrom, to: mainTo } = firstSeries!.time; - const { - time: { from: seriesFrom, to: seriesTo }, - } = series; + const startDate = parseAbsoluteDate(seriesFrom ?? mainFrom)!; + const endDate = parseAbsoluteDate(seriesTo ?? mainTo, { roundUp: true })!; - const startDate = parseAbsoluteDate(seriesFrom ?? from)!; - const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!; + const getTotalDuration = () => { + const mainStartDate = parseAbsoluteDate(mainFrom)!; + const mainEndDate = parseAbsoluteDate(mainTo, { roundUp: true })!; + return mainEndDate.diff(mainStartDate, 'millisecond'); + }; - const onStartChange = (newDate: Moment) => { - if (reportType === 'kpi-over-time') { - const mainStartDate = parseAbsoluteDate(from)!; - const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; - const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); - const newFrom = newDate.toISOString(); - const newTo = newDate.add(totalDuration, 'millisecond').toISOString(); + const onStartChange = (newStartDate: Moment) => { + if (reportType === ReportTypes.KPI) { + const totalDuration = getTotalDuration(); + const newFrom = newStartDate.toISOString(); + const newTo = newStartDate.add(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newFrom = newDate.toISOString(); + const newFrom = newStartDate.toISOString(); setSeries(seriesId, { ...series, @@ -55,20 +55,19 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) { }); } }; - const onEndChange = (newDate: Moment) => { - if (reportType === 'kpi-over-time') { - const mainStartDate = parseAbsoluteDate(from)!; - const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; - const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); - const newTo = newDate.toISOString(); - const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString(); + + const onEndChange = (newEndDate: Moment) => { + if (reportType === ReportTypes.KPI) { + const totalDuration = getTotalDuration(); + const newTo = newEndDate.toISOString(); + const newFrom = newEndDate.subtract(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newTo = newDate.toISOString(); + const newTo = newEndDate.toISOString(); setSeries(seriesId, { ...series, @@ -90,7 +89,7 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) { aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', { defaultMessage: 'Start date', })} - dateFormat={dateFormat} + dateFormat={dateFormat.replace('ss.SSS', 'ss')} showTimeSelect /> } @@ -104,7 +103,7 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) { aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', { defaultMessage: 'End date', })} - dateFormat={dateFormat} + dateFormat={dateFormat.replace('ss.SSS', 'ss')} showTimeSelect /> } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx index 3566835b1701..d17e451ef702 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx @@ -10,19 +10,19 @@ import { isEmpty } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { LOADING_VIEW } from '../series_builder/series_builder'; -import { SeriesUrl } from '../types'; +import { LOADING_VIEW } from '../series_editor/series_editor'; +import { ReportViewType, SeriesUrl } from '../types'; export function EmptyView({ loading, - height, series, + reportType, }: { loading: boolean; - height: string; - series: SeriesUrl; + series?: SeriesUrl; + reportType: ReportViewType; }) { - const { dataType, reportType, reportDefinitions } = series ?? {}; + const { dataType, reportDefinitions } = series ?? {}; let emptyMessage = EMPTY_LABEL; @@ -45,7 +45,7 @@ export function EmptyView({ } return ( - + {loading && ( ` +const Wrapper = styled.div` text-align: center; - height: ${(props) => props.height}; position: relative; `; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx index fe2953edd36d..03fd23631f75 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; import { FilterLabel } from './filter_label'; import * as useSeriesHook from '../hooks/use_series_filters'; import { buildFilterLabel } from '../../filter_value_label/filter_value_label'; @@ -27,9 +27,10 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={'kpi-over-time'} + seriesId={0} removeFilter={jest.fn()} indexPattern={mockIndexPattern} + series={mockUxSeries} /> ); @@ -51,9 +52,10 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={'kpi-over-time'} + seriesId={0} removeFilter={removeFilter} indexPattern={mockIndexPattern} + series={mockUxSeries} /> ); @@ -74,9 +76,10 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={'kpi-over-time'} + seriesId={0} removeFilter={removeFilter} indexPattern={mockIndexPattern} + series={mockUxSeries} /> ); @@ -100,9 +103,10 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={true} - seriesId={'kpi-over-time'} + seriesId={0} removeFilter={jest.fn()} indexPattern={mockIndexPattern} + series={mockUxSeries} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx index a08e777c5ea7..c6254a85de9a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -9,21 +9,24 @@ import React from 'react'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { FilterValueLabel } from '../../filter_value_label/filter_value_label'; +import { SeriesUrl } from '../types'; interface Props { field: string; label: string; - value: string; - seriesId: string; + value: string | string[]; + seriesId: number; + series: SeriesUrl; negate: boolean; definitionFilter?: boolean; indexPattern: IndexPattern; - removeFilter: (field: string, value: string, notVal: boolean) => void; + removeFilter: (field: string, value: string | string[], notVal: boolean) => void; } export function FilterLabel({ label, seriesId, + series, field, value, negate, @@ -31,7 +34,7 @@ export function FilterLabel({ removeFilter, definitionFilter, }: Props) { - const { invertFilter } = useSeriesFilters({ seriesId }); + const { invertFilter } = useSeriesFilters({ seriesId, series }); return indexPattern ? ( { + setSeries(seriesId, { ...series, color: colorN }); + }; + + const color = + series.color ?? (theme.eui as unknown as Record)[`euiColorVis${seriesId}`]; + + const button = ( + + setIsOpen((prevState) => !prevState)} flush="both"> + + + + ); + + return ( + setIsOpen(false)}> + + + + + ); +} + +const PICK_A_COLOR_LABEL = i18n.translate( + 'xpack.observability.overview.exploratoryView.pickColor', + { + defaultMessage: 'Pick a color', + } +); + +const EDIT_SERIES_COLOR_LABEL = i18n.translate( + 'xpack.observability.overview.exploratoryView.editSeriesColor', + { + defaultMessage: 'Edit color for series', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx similarity index 54% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx index e21da424b58c..e02f11dfc495 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx @@ -6,11 +6,13 @@ */ import { EuiSuperDatePicker } from '@elastic/eui'; -import React, { useEffect } from 'react'; -import { useHasData } from '../../../../hooks/use_has_data'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges'; -import { DEFAULT_TIME } from '../configurations/constants'; +import React from 'react'; + +import { useHasData } from '../../../../../hooks/use_has_data'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { useQuickTimeRanges } from '../../../../../hooks/use_quick_time_ranges'; +import { SeriesUrl } from '../../types'; +import { ReportTypes } from '../../configurations/constants'; export interface TimePickerTime { from: string; @@ -22,28 +24,27 @@ export interface TimePickerQuickRange extends TimePickerTime { } interface Props { - seriesId: string; + seriesId: number; + series: SeriesUrl; } -export function SeriesDatePicker({ seriesId }: Props) { +export function SeriesDatePicker({ series, seriesId }: Props) { const { onRefreshTimeRange } = useHasData(); const commonlyUsedRanges = useQuickTimeRanges(); - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); + const { setSeries, reportType, allSeries } = useSeriesStorage(); function onTimeChange({ start, end }: { start: string; end: string }) { onRefreshTimeRange(); - setSeries(seriesId, { ...series, time: { from: start, to: end } }); - } - - useEffect(() => { - if (!series || !series.time) { - setSeries(seriesId, { ...series, time: DEFAULT_TIME }); + if (reportType === ReportTypes.KPI) { + allSeries.forEach((currSeries, seriesIndex) => { + setSeries(seriesIndex, { ...currSeries, time: { from: start, to: end } }); + }); + } else { + setSeries(seriesId, { ...series, time: { from: start, to: end } }); } - }, [series, seriesId, setSeries]); + } return ( , { initSeries }); + const { getByText } = render(, { + initSeries, + }); getByText('Last 30 minutes'); }); - it('should set defaults', async function () { - const initSeries = { - data: { - 'uptime-pings-histogram': { - reportType: 'kpi-over-time' as const, - dataType: 'synthetics' as const, - breakdown: 'monitor.status', - }, - }, - }; - const { setSeries: setSeries1 } = render( - , - { initSeries: initSeries as any } - ); - expect(setSeries1).toHaveBeenCalledTimes(1); - expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', { - breakdown: 'monitor.status', - dataType: 'synthetics' as const, - reportType: 'kpi-over-time' as const, - time: DEFAULT_TIME, - }); - }); - it('should set series data', async function () { const initSeries = { - data: { - 'uptime-pings-histogram': { + data: [ + { + name: 'uptime-pings-histogram', dataType: 'synthetics' as const, - reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, - }, + ], }; const { onRefreshTimeRange } = mockUseHasData(); - const { getByTestId, setSeries } = render(, { - initSeries, - }); + const { getByTestId, setSeries } = render( + , + { + initSeries, + } + ); await waitFor(function () { fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton')); @@ -76,10 +57,10 @@ describe('SeriesDatePicker', function () { expect(onRefreshTimeRange).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith('series-id', { + expect(setSeries).toHaveBeenCalledWith(0, { + name: 'uptime-pings-histogram', breakdown: 'monitor.status', dataType: 'synthetics', - reportType: 'kpi-over-time', time: { from: 'now/d', to: 'now/d' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index ba1f2214223e..bf5feb7d5863 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -94,6 +94,19 @@ export const DataViewLabels: Record = { 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL, }; +export enum ReportTypes { + KPI = 'kpi-over-time', + DISTRIBUTION = 'data-distribution', + CORE_WEB_VITAL = 'core-web-vitals', + DEVICE_DISTRIBUTION = 'device-data-distribution', +} + +export enum DataTypes { + SYNTHETICS = 'synthetics', + UX = 'ux', + MOBILE = 'mobile', +} + export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; export const FILTER_RECORDS = 'FILTER_RECORDS'; export const TERMS_COLUMN = 'TERMS_COLUMN'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts index 6f990015fbc6..55ac75b47c05 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts @@ -8,10 +8,12 @@ export enum URL_KEYS { DATA_TYPE = 'dt', OPERATION_TYPE = 'op', - REPORT_TYPE = 'rt', SERIES_TYPE = 'st', BREAK_DOWN = 'bd', FILTERS = 'ft', REPORT_DEFINITIONS = 'rdf', SELECTED_METRIC = 'mt', + HIDDEN = 'h', + NAME = 'n', + COLOR = 'c', } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 574a9f6a2bc1..3f6551986527 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -15,6 +15,7 @@ import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config'; import { getMobileKPIConfig } from './mobile/kpi_over_time_config'; import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; +import { DataTypes, ReportTypes } from './constants'; interface Props { reportType: ReportViewType; @@ -24,24 +25,24 @@ interface Props { export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { switch (dataType) { - case 'ux': - if (reportType === 'data-distribution') { + case DataTypes.UX: + if (reportType === ReportTypes.DISTRIBUTION) { return getRumDistributionConfig({ indexPattern }); } - if (reportType === 'core-web-vitals') { + if (reportType === ReportTypes.CORE_WEB_VITAL) { return getCoreWebVitalsConfig({ indexPattern }); } return getKPITrendsLensConfig({ indexPattern }); - case 'synthetics': - if (reportType === 'data-distribution') { + case DataTypes.SYNTHETICS: + if (reportType === ReportTypes.DISTRIBUTION) { return getSyntheticsDistributionConfig({ indexPattern }); } return getSyntheticsKPIConfig({ indexPattern }); - case 'mobile': - if (reportType === 'data-distribution') { + case DataTypes.MOBILE: + if (reportType === ReportTypes.DISTRIBUTION) { return getMobileKPIDistributionConfig({ indexPattern }); } - if (reportType === 'device-data-distribution') { + if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { return getMobileDeviceDistributionConfig({ indexPattern }); } return getMobileKPIConfig({ indexPattern }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 706c58609b7c..9e7c5254b511 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -16,7 +16,7 @@ import { } from './constants/elasticsearch_fieldnames'; import { buildExistsFilter, buildPhrasesFilter } from './utils'; import { sampleAttributeKpi } from './test_data/sample_attribute_kpi'; -import { REPORT_METRIC_FIELD } from './constants'; +import { RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from './constants'; describe('Lens Attribute', () => { mockAppIndexPattern(); @@ -38,6 +38,9 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: {}, time: { from: 'now-15m', to: 'now' }, + color: 'green', + name: 'test-series', + selectedMetricField: TRANSACTION_DURATION, }; beforeEach(() => { @@ -50,7 +53,7 @@ describe('Lens Attribute', () => { it('should return expected json for kpi report type', function () { const seriesConfigKpi = getDefaultConfigs({ - reportType: 'kpi-over-time', + reportType: ReportTypes.KPI, dataType: 'ux', indexPattern: mockIndexPattern, }); @@ -63,6 +66,9 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'service.name': ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, + color: 'green', + name: 'test-series', + selectedMetricField: RECORDS_FIELD, }, ]); @@ -135,6 +141,9 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, + color: 'green', + name: 'test-series', + selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -383,7 +392,7 @@ describe('Lens Attribute', () => { palette: undefined, seriesType: 'line', xAccessor: 'x-axis-column-layer0', - yConfig: [{ forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], }, ], legend: { isVisible: true, position: 'right' }, @@ -403,6 +412,9 @@ describe('Lens Attribute', () => { reportDefinitions: { 'performance.metric': [LCP_FIELD] }, breakdown: USER_AGENT_NAME, time: { from: 'now-15m', to: 'now' }, + color: 'green', + name: 'test-series', + selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -423,7 +435,7 @@ describe('Lens Attribute', () => { seriesType: 'line', splitAccessor: 'breakdown-column-layer0', xAccessor: 'x-axis-column-layer0', - yConfig: [{ forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], }, ]); @@ -589,6 +601,9 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, + color: 'green', + name: 'test-series', + selectedMetricField: TRANSACTION_DURATION, }; const filters = lnsAttr.getLayerFilters(layerConfig1, 2); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 2778edc94838..ec2e6b5066c8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -37,10 +37,11 @@ import { REPORT_METRIC_FIELD, RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD, + ReportTypes, } from './constants'; import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; import { PersistableFilter } from '../../../../../../lens/common'; -import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; +import { parseAbsoluteDate } from '../components/date_range_picker'; import { getDistributionInPercentageColumn } from './lens_columns/overall_column'; function getLayerReferenceName(layerId: string) { @@ -74,14 +75,6 @@ export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricF timeScale = currField?.timeScale; columnLabel = currField?.label; } - } else if (metricOptions?.[0].field || metricOptions?.[0].id) { - const firstMetricOption = metricOptions?.[0]; - - selectedMetricField = firstMetricOption.field || firstMetricOption.id; - columnType = firstMetricOption.columnType; - columnFilters = firstMetricOption.columnFilters; - timeScale = firstMetricOption.timeScale; - columnLabel = firstMetricOption.label; } return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel }; @@ -96,7 +89,9 @@ export interface LayerConfig { reportDefinitions: URLReportDefinition; time: { to: string; from: string }; indexPattern: IndexPattern; - selectedMetricField?: string; + selectedMetricField: string; + color: string; + name: string; } export class LensAttributes { @@ -467,14 +462,15 @@ export class LensAttributes { getLayerFilters(layerConfig: LayerConfig, totalLayers: number) { const { filters, - time: { from, to }, + time, seriesConfig: { baseFilters: layerFilters, reportType }, } = layerConfig; let baseFilters = ''; - if (reportType !== 'kpi-over-time' && totalLayers > 1) { + + if (reportType !== ReportTypes.KPI && totalLayers > 1 && time) { // for kpi over time, we don't need to add time range filters // since those are essentially plotted along the x-axis - baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`; + baseFilters += `@timestamp >= ${time.from} and @timestamp <= ${time.to}`; } layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => { @@ -530,7 +526,11 @@ export class LensAttributes { } getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { - if (index === 0 || mainLayerConfig.seriesConfig.reportType !== 'kpi-over-time') { + if ( + index === 0 || + mainLayerConfig.seriesConfig.reportType !== ReportTypes.KPI || + !layerConfig.time + ) { return null; } @@ -542,11 +542,14 @@ export class LensAttributes { time: { from }, } = layerConfig; - const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'); + const inDays = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days')); if (inDays > 1) { return inDays + 'd'; } - const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'); + const inHours = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours')); + if (inHours === 0) { + return null; + } return inHours + 'h'; } @@ -564,6 +567,8 @@ export class LensAttributes { const { sourceField } = seriesConfig.xAxisColumn; + const label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label; + layers[layerId] = { columnOrder: [ `x-axis-column-${layerId}`, @@ -577,7 +582,7 @@ export class LensAttributes { [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), [`y-axis-column-${layerId}`]: { ...mainYAxis, - label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label, + label, filter: { query: columnFilter, language: 'kuery' }, ...(timeShift ? { timeShift } : {}), }, @@ -621,7 +626,7 @@ export class LensAttributes { seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType, palette: layerConfig.seriesConfig.palette, yConfig: layerConfig.seriesConfig.yConfig || [ - { forAccessor: `y-axis-column-layer${index}` }, + { forAccessor: `y-axis-column-layer${index}`, color: layerConfig.color }, ], xAccessor: `x-axis-column-layer${index}`, ...(layerConfig.breakdown && @@ -635,7 +640,7 @@ export class LensAttributes { }; } - getJSON(): TypedLensByValueInput['attributes'] { + getJSON(refresh?: number): TypedLensByValueInput['attributes'] { const uniqueIndexPatternsIds = Array.from( new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)]) ); @@ -644,7 +649,7 @@ export class LensAttributes { return { title: 'Prefilled from exploratory view app', - description: '', + description: String(refresh), visualizationType: 'lnsXY', references: [ ...uniqueIndexPatternsIds.map((patternId) => ({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index d1612a08f555..4e178bba7e02 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, REPORT_METRIC_FIELD, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, ReportTypes, USE_BREAK_DOWN_COLUMN } from '../constants'; import { buildPhraseFilter } from '../utils'; import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames'; import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels'; @@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: 'device-data-distribution', + reportType: ReportTypes.DEVICE_DISTRIBUTION, defaultSeriesType: 'bar', seriesTypes: ['bar', 'bar_horizontal'], xAxisColumn: { @@ -38,13 +38,13 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, + definitionFields: [SERVICE_NAME], metricOptions: [ { - id: 'labels.device_id', field: 'labels.device_id', + id: 'labels.device_id', label: NUMBER_OF_DEVICES, }, ], - definitionFields: [SERVICE_NAME], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts index 9b1c4c8da3e9..1da27be4fcc9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -21,7 +21,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: 'data-distribution', + reportType: ReportTypes.DISTRIBUTION, defaultSeriesType: 'bar', seriesTypes: ['line', 'bar'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts index 945a631078a3..3ee5b3125fcd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts @@ -6,7 +6,13 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; +import { + FieldLabels, + OPERATION_COLUMN, + RECORDS_FIELD, + REPORT_METRIC_FIELD, + ReportTypes, +} from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -26,7 +32,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: 'kpi-over-time', + reportType: ReportTypes.KPI, defaultSeriesType: 'line', seriesTypes: ['line', 'bar', 'area'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts index 07bb13f957e4..35e094996f6f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts @@ -9,7 +9,7 @@ import { mockAppIndexPattern, mockIndexPattern } from '../../rtl_helpers'; import { getDefaultConfigs } from '../default_configs'; import { LayerConfig, LensAttributes } from '../lens_attributes'; import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv'; -import { SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; +import { LCP_FIELD, SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; describe('Core web vital config test', function () { mockAppIndexPattern(); @@ -24,10 +24,13 @@ describe('Core web vital config test', function () { const layerConfig: LayerConfig = { seriesConfig, + color: 'green', + name: 'test-series', + breakdown: USER_AGENT_OS, indexPattern: mockIndexPattern, - reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, - breakdown: USER_AGENT_OS, + reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, + selectedMetricField: LCP_FIELD, }; beforeEach(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts index 62455df24808..e8d620388a89 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -11,6 +11,7 @@ import { FieldLabels, FILTER_RECORDS, REPORT_METRIC_FIELD, + ReportTypes, USE_BREAK_DOWN_COLUMN, } from '../constants'; import { buildPhraseFilter } from '../utils'; @@ -38,7 +39,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_horizontal_percentage_stacked', - reportType: 'core-web-vitals', + reportType: ReportTypes.CORE_WEB_VITAL, seriesTypes: ['bar_horizontal_percentage_stacked'], xAxisColumn: { sourceField: USE_BREAK_DOWN_COLUMN, @@ -153,5 +154,6 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon { color: statusPallete[1], forAccessor: 'y-axis-column-1' }, { color: statusPallete[2], forAccessor: 'y-axis-column-2' }, ], + query: { query: 'transaction.type: "page-load"', language: 'kuery' }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts index f34c8db6c197..de6f2c67b2ae 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts @@ -6,7 +6,12 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; +import { + FieldLabels, + REPORT_METRIC_FIELD, + RECORDS_PERCENTAGE_FIELD, + ReportTypes, +} from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -41,7 +46,7 @@ import { export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: 'data-distribution', + reportType: ReportTypes.DISTRIBUTION, defaultSeriesType: 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts index 5899b16d12b4..9112778eadaa 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts @@ -6,7 +6,13 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; +import { + FieldLabels, + OPERATION_COLUMN, + RECORDS_FIELD, + REPORT_METRIC_FIELD, + ReportTypes, +} from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -43,7 +49,7 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_stacked', seriesTypes: [], - reportType: 'kpi-over-time', + reportType: ReportTypes.KPI, xAxisColumn: { sourceField: '@timestamp', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index 730e742f9d8c..da90f45d1520 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -6,7 +6,12 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; +import { + FieldLabels, + REPORT_METRIC_FIELD, + RECORDS_PERCENTAGE_FIELD, + ReportTypes, +} from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -30,7 +35,7 @@ export function getSyntheticsDistributionConfig({ indexPattern, }: ConfigProps): SeriesConfig { return { - reportType: 'data-distribution', + reportType: ReportTypes.DISTRIBUTION, defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 4ee22181d433..65b43a83a8fb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD } from '../constants'; +import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -30,7 +30,7 @@ const SUMMARY_DOWN = 'summary.down'; export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: 'kpi-over-time', + reportType: ReportTypes.KPI, defaultSeriesType: 'bar_stacked', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 596e7af4378e..7e0ea1e57548 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -5,12 +5,18 @@ * 2.0. */ export const sampleAttribute = { - title: 'Prefilled from exploratory view app', - description: '', - visualizationType: 'lnsXY', + description: 'undefined', references: [ - { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, - { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, + { + id: 'apm-*', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'apm-*', + name: 'indexpattern-datasource-layer-layer0', + type: 'index-pattern', + }, ], state: { datasourceStates: { @@ -28,17 +34,23 @@ export const sampleAttribute = { ], columns: { 'x-axis-column-layer0': { - sourceField: 'transaction.duration.us', - label: 'Page load time', dataType: 'number', - operationType: 'range', isBucketed: true, - scale: 'interval', + label: 'Page load time', + operationType: 'range', params: { - type: 'histogram', - ranges: [{ from: 0, to: 1000, label: '' }], maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', }, + scale: 'interval', + sourceField: 'transaction.duration.us', }, 'y-axis-column-layer0': { dataType: 'number', @@ -81,16 +93,16 @@ export const sampleAttribute = { 'y-axis-column-layer0X1': { customLabel: true, dataType: 'number', - isBucketed: false, - label: 'Part of count() / overall_sum(count())', - operationType: 'count', - scale: 'ratio', - sourceField: 'Records', filter: { language: 'kuery', query: 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, + isBucketed: false, + label: 'Part of count() / overall_sum(count())', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', }, 'y-axis-column-layer0X2': { customLabel: true, @@ -140,27 +152,52 @@ export const sampleAttribute = { }, }, }, + filters: [], + query: { + language: 'kuery', + query: 'transaction.duration.us < 60000000', + }, visualization: { - legend: { isVisible: true, position: 'right' }, - valueLabels: 'hide', - fittingFunction: 'Linear', + axisTitlesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, curveType: 'CURVE_MONOTONE_X', - axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, - tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, - gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, - preferredSeriesType: 'line', + fittingFunction: 'Linear', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', layerType: 'data', seriesType: 'line', - yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', + yConfig: [ + { + color: 'green', + forAccessor: 'y-axis-column-layer0', + }, + ], }, ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'hide', }, - query: { query: 'transaction.duration.us < 60000000', language: 'kuery' }, - filters: [], }, + title: 'Prefilled from exploratory view app', + visualizationType: 'lnsXY', }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts index 56ceba8fc52d..dff3d6b3ad5e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -5,7 +5,7 @@ * 2.0. */ export const sampleAttributeCoreWebVital = { - description: '', + description: 'undefined', references: [ { id: 'apm-*', @@ -94,7 +94,7 @@ export const sampleAttributeCoreWebVital = { filters: [], query: { language: 'kuery', - query: '', + query: 'transaction.type: "page-load"', }, visualization: { axisTitlesVisibilitySettings: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts index 72933573c410..6ed9b4face6e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -5,12 +5,18 @@ * 2.0. */ export const sampleAttributeKpi = { - title: 'Prefilled from exploratory view app', - description: '', - visualizationType: 'lnsXY', + description: 'undefined', references: [ - { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, - { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, + { + id: 'apm-*', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'apm-*', + name: 'indexpattern-datasource-layer-layer0', + type: 'index-pattern', + }, ], state: { datasourceStates: { @@ -20,25 +26,27 @@ export const sampleAttributeKpi = { columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], columns: { 'x-axis-column-layer0': { - sourceField: '@timestamp', dataType: 'date', isBucketed: true, label: '@timestamp', operationType: 'date_histogram', - params: { interval: 'auto' }, + params: { + interval: 'auto', + }, scale: 'interval', + sourceField: '@timestamp', }, 'y-axis-column-layer0': { dataType: 'number', + filter: { + language: 'kuery', + query: 'transaction.type: page-load and processor.event: transaction', + }, isBucketed: false, label: 'Page views', operationType: 'count', scale: 'ratio', sourceField: 'Records', - filter: { - query: 'transaction.type: page-load and processor.event: transaction', - language: 'kuery', - }, }, }, incompleteColumns: {}, @@ -46,27 +54,52 @@ export const sampleAttributeKpi = { }, }, }, + filters: [], + query: { + language: 'kuery', + query: '', + }, visualization: { - legend: { isVisible: true, position: 'right' }, - valueLabels: 'hide', - fittingFunction: 'Linear', + axisTitlesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, curveType: 'CURVE_MONOTONE_X', - axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, - tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, - gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, - preferredSeriesType: 'line', + fittingFunction: 'Linear', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', layerType: 'data', seriesType: 'line', - yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', + yConfig: [ + { + color: 'green', + forAccessor: 'y-axis-column-layer0', + }, + ], }, ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'hide', }, - query: { query: '', language: 'kuery' }, - filters: [], }, + title: 'Prefilled from exploratory view app', + visualizationType: 'lnsXY', }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index c7d2d21581e7..56e6cb521035 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; -import type { SeriesUrl, UrlFilter } from '../types'; +import type { ReportViewType, SeriesUrl, UrlFilter } from '../types'; import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage'; import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public'; @@ -16,40 +16,43 @@ export function convertToShortUrl(series: SeriesUrl) { const { operationType, seriesType, - reportType, breakdown, filters, reportDefinitions, dataType, selectedMetricField, + hidden, + name, + color, ...restSeries } = series; return { [URL_KEYS.OPERATION_TYPE]: operationType, - [URL_KEYS.REPORT_TYPE]: reportType, [URL_KEYS.SERIES_TYPE]: seriesType, [URL_KEYS.BREAK_DOWN]: breakdown, [URL_KEYS.FILTERS]: filters, [URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions, [URL_KEYS.DATA_TYPE]: dataType, [URL_KEYS.SELECTED_METRIC]: selectedMetricField, + [URL_KEYS.HIDDEN]: hidden, + [URL_KEYS.NAME]: name, + [URL_KEYS.COLOR]: color, ...restSeries, }; } -export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { - const allSeriesIds = Object.keys(allSeries); - - const allShortSeries: AllShortSeries = {}; - - allSeriesIds.forEach((seriesKey) => { - allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]); - }); +export function createExploratoryViewUrl( + { reportType, allSeries }: { reportType: ReportViewType; allSeries: AllSeries }, + baseHref = '' +) { + const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series)); return ( baseHref + - `/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}` + `/app/observability/exploratory-view/#?reportType=${reportType}&sr=${rison.encode( + allShortSeries as unknown as RisonValue + )}` ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index a3b5130e9830..8f061fcbfbf2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -6,12 +6,18 @@ */ import React from 'react'; -import { screen, waitFor } from '@testing-library/dom'; +import { screen } from '@testing-library/dom'; import { render, mockAppIndexPattern } from './rtl_helpers'; import { ExploratoryView } from './exploratory_view'; import * as obsvInd from './utils/observability_index_patterns'; +import * as pluginHook from '../../../hooks/use_plugin_context'; import { createStubIndexPattern } from '../../../../../../../src/plugins/data/common/stubs'; +jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ + appMountParameters: { + setHeaderActionMenu: jest.fn(), + }, +} as any); describe('ExploratoryView', () => { mockAppIndexPattern(); @@ -40,36 +46,22 @@ describe('ExploratoryView', () => { }); it('renders exploratory view', async () => { - render(); + render(, { initSeries: { data: [] } }); - expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); + expect(await screen.findByText(/No series found. Please add a series./i)).toBeInTheDocument(); + expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument(); + expect(await screen.findByText(/Refresh/i)).toBeInTheDocument(); expect( await screen.findByRole('heading', { name: /Performance Distribution/i }) ).toBeInTheDocument(); }); it('renders lens component when there is series', async () => { - const initSeries = { - data: { - 'ux-series': { - isNew: true, - dataType: 'ux' as const, - reportType: 'data-distribution' as const, - breakdown: 'user_agent .name', - reportDefinitions: { 'service.name': ['elastic-co'] }, - time: { from: 'now-15m', to: 'now' }, - }, - }, - }; - - render(, { initSeries }); + render(); - expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument(); expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument(); - await waitFor(() => { - screen.getByRole('table', { name: /this table contains 1 rows\./i }); - }); + expect(screen.getByTestId('exploratoryViewSeriesPanel0')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index af04108c5679..faf064868dec 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -4,11 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef, useState } from 'react'; -import { EuiPanel, EuiTitle } from '@elastic/eui'; +import { EuiButtonEmpty, EuiPanel, EuiResizableContainer, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; -import { isEmpty } from 'lodash'; +import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; @@ -16,40 +17,15 @@ import { useSeriesStorage } from './hooks/use_series_storage'; import { useLensAttributes } from './hooks/use_lens_attributes'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; -import { SeriesBuilder } from './series_builder/series_builder'; -import { SeriesUrl } from './types'; +import { SeriesViews } from './views/series_views'; import { LensEmbeddable } from './lens_embeddable'; import { EmptyView } from './components/empty_view'; -export const combineTimeRanges = ( - allSeries: Record, - firstSeries?: SeriesUrl -) => { - let to: string = ''; - let from: string = ''; - if (firstSeries?.reportType === 'kpi-over-time') { - return firstSeries.time; - } - Object.values(allSeries ?? {}).forEach((series) => { - if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) { - const seriesTo = new Date(series.time.to); - const seriesFrom = new Date(series.time.from); - if (!to || seriesTo > new Date(to)) { - to = series.time.to; - } - if (!from || seriesFrom < new Date(from)) { - from = series.time.from; - } - } - }); - return { to, from }; -}; +export type PanelId = 'seriesPanel' | 'chartPanel'; export function ExploratoryView({ saveAttributes, - multiSeries, }: { - multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { const { @@ -69,20 +45,19 @@ export function ExploratoryView({ const { loadIndexPattern, loading } = useAppIndexPatternContext(); - const { firstSeries, firstSeriesId, allSeries } = useSeriesStorage(); + const { firstSeries, allSeries, lastRefresh, reportType } = useSeriesStorage(); const lensAttributesT = useLensAttributes(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { const headerOffset = wrapperRef.current.getBoundingClientRect().top; - const seriesOffset = seriesBuilderRef.current.getBoundingClientRect().height; - setHeight(`calc(100vh - ${seriesOffset + headerOffset + 40}px)`); + setHeight(`calc(100vh - ${headerOffset + 40}px)`); } }; useEffect(() => { - Object.values(allSeries).forEach((seriesT) => { + allSeries.forEach((seriesT) => { loadIndexPattern({ dataType: seriesT.dataType, }); @@ -96,38 +71,102 @@ export function ExploratoryView({ } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(lensAttributesT ?? {})]); + }, [JSON.stringify(lensAttributesT ?? {}), lastRefresh]); useEffect(() => { setHeightOffset(); }); + const collapseFn = useRef<(id: PanelId, direction: PanelDirection) => void>(); + + const [hiddenPanel, setHiddenPanel] = useState(''); + + const onCollapse = (panelId: string) => { + setHiddenPanel((prevState) => (panelId === prevState ? '' : panelId)); + }; + + const onChange = (panelId: PanelId) => { + onCollapse(panelId); + if (collapseFn.current) { + collapseFn.current(panelId, panelId === 'seriesPanel' ? 'right' : 'left'); + } + }; + return ( {lens ? ( <> - + - {lensAttributes ? ( - - ) : ( - + + {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { + collapseFn.current = (id, direction) => togglePanel?.(id, { direction }); + + return ( + <> + + {lensAttributes ? ( + + ) : ( + + )} + + + + {hiddenPanel === 'chartPanel' ? ( + onChange('chartPanel')} iconType="arrowDown"> + {SHOW_CHART_LABEL} + + ) : ( + onChange('chartPanel')} + iconType="arrowUp" + color="text" + > + {HIDE_CHART_LABEL} + + )} + + + + ); + }} + + {hiddenPanel === 'seriesPanel' && ( + onChange('seriesPanel')} iconType="arrowUp"> + {PREVIEW_LABEL} + )} - ) : ( -

- {i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', { - defaultMessage: - 'Lens app is not available, please enable Lens to use exploratory view.', - })} -

+

{LENS_NOT_AVAILABLE}

)}
@@ -147,4 +186,39 @@ const Wrapper = styled(EuiPanel)` margin: 0 auto; width: 100%; overflow-x: auto; + position: relative; +`; + +const ShowPreview = styled(EuiButtonEmpty)` + position: absolute; + bottom: 34px; +`; +const HideChart = styled(EuiButtonEmpty)` + position: absolute; + top: -35px; + right: 50px; `; +const ShowChart = styled(EuiButtonEmpty)` + position: absolute; + top: -10px; + right: 50px; +`; + +const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', { + defaultMessage: 'Hide chart', +}); + +const SHOW_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.showChart', { + defaultMessage: 'Show chart', +}); + +const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', { + defaultMessage: 'Preview', +}); + +const LENS_NOT_AVAILABLE = i18n.translate( + 'xpack.observability.overview.exploratoryView.lensDisabled', + { + defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx index 619ea0d21ae1..b8f16f3e5eff 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx @@ -23,14 +23,15 @@ describe('AddToCaseAction', function () { it('should be able to click add to case button', async function () { const initSeries = { - data: { - 'uptime-pings-histogram': { + data: [ + { + name: 'test-series', dataType: 'synthetics' as const, reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, - }, + ], }; const { findByText, core } = render( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx index 4fa8deb2700d..bc813a4980e7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx @@ -17,7 +17,7 @@ import { Case, SubCase } from '../../../../../../cases/common'; import { observabilityFeatureId } from '../../../../../common'; export interface AddToCaseProps { - timeRange: { from: string; to: string }; + timeRange?: { from: string; to: string }; lensAttributes: TypedLensByValueInput['attributes'] | null; } @@ -54,6 +54,7 @@ export function AddToCaseAction({ lensAttributes, timeRange }: AddToCaseProps) { return ( <> ); - getByText('Open in Lens'); - }); - - it('should be able to click open in lens', function () { - const initSeries = { - data: { - 'uptime-pings-histogram': { - dataType: 'synthetics' as const, - reportType: 'kpi-over-time' as const, - breakdown: 'monitor.status', - time: { from: 'now-15m', to: 'now' }, - }, - }, - }; - - const { getByText, core } = render( - , - { initSeries } - ); - fireEvent.click(getByText('Open in Lens')); - - expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); - expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( - { - attributes: { title: 'Performance distribution' }, - id: '', - timeRange: { - from: 'now-15m', - to: 'now', - }, - }, - { openInNewTab: true } - ); + getByText('Refresh'); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index 7adef4779ea9..bec8673f88b4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -5,44 +5,37 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { TypedLensByValueInput, LensEmbeddableInput } from '../../../../../../lens/public'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { TypedLensByValueInput } from '../../../../../../lens/public'; import { DataViewLabels } from '../configurations/constants'; -import { ObservabilityAppServices } from '../../../../application/types'; import { useSeriesStorage } from '../hooks/use_series_storage'; -import { combineTimeRanges } from '../exploratory_view'; -import { AddToCaseAction } from './add_to_case_action'; +import { LastUpdated } from './last_updated'; +import { combineTimeRanges } from '../lens_embeddable'; +import { ExpViewActionMenu } from '../components/action_menu'; interface Props { - seriesId: string; + seriesId?: number; + lastUpdated?: number; lensAttributes: TypedLensByValueInput['attributes'] | null; } -export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { - const kServices = useKibana().services; +export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: Props) { + const { getSeries, allSeries, setLastRefresh, reportType } = useSeriesStorage(); - const { lens } = kServices; + const series = seriesId ? getSeries(seriesId) : undefined; - const { getSeries, allSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - - const [isSaveOpen, setIsSaveOpen] = useState(false); - - const LensSaveModalComponent = lens.SaveModalComponent; - - const timeRange = combineTimeRanges(allSeries, series); + const timeRange = combineTimeRanges(reportType, allSeries, series); return ( <> +

- {DataViewLabels[series.reportType] ?? + {DataViewLabels[reportType] ?? i18n.translate('xpack.observability.expView.heading.label', { defaultMessage: 'Analyze data', })}{' '} @@ -58,58 +51,18 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { - + - { - if (lensAttributes) { - lens.navigateToPrefilledEditor( - { - id: '', - timeRange, - attributes: lensAttributes, - }, - { - openInNewTab: true, - } - ); - } - }} - > - {i18n.translate('xpack.observability.expView.heading.openInLens', { - defaultMessage: 'Open in Lens', - })} - - - - { - if (lensAttributes) { - setIsSaveOpen(true); - } - }} - > - {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { - defaultMessage: 'Save', - })} + setLastRefresh(Date.now())}> + {REFRESH_LABEL} - - {isSaveOpen && lensAttributes && ( - setIsSaveOpen(false)} - onSave={() => {}} - /> - )} ); } + +const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', { + defaultMessage: 'Refresh', +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx similarity index 55% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx index 874171de123d..c352ec0423dd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx @@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react'; import { EuiIcon, EuiText } from '@elastic/eui'; import moment from 'moment'; +import { FormattedMessage } from '@kbn/i18n/react'; interface Props { lastUpdated?: number; @@ -18,20 +19,34 @@ export function LastUpdated({ lastUpdated }: Props) { useEffect(() => { const interVal = setInterval(() => { setRefresh(Date.now()); - }, 1000); + }, 5000); return () => { clearInterval(interVal); }; }, []); + useEffect(() => { + setRefresh(Date.now()); + }, [lastUpdated]); + if (!lastUpdated) { return null; } + const isWarning = moment().diff(moment(lastUpdated), 'minute') > 5; + const isDanger = moment().diff(moment(lastUpdated), 'minute') > 10; + return ( - - Last Updated: {moment(lastUpdated).from(refresh)} + + + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts index 5ec9e1d4ab4b..d1e15aa916ee 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts @@ -25,7 +25,7 @@ async function addToCase( http: HttpSetup, theCase: Case | SubCase, attributes: TypedLensByValueInput['attributes'], - timeRange: { from: string; to: string } + timeRange?: { from: string; to: string } ) { const apiPath = `/api/cases/${theCase?.id}/comments`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index 88818665bbe2..83a7ac1ae17d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -27,7 +27,7 @@ interface ProviderProps { } type HasAppDataState = Record; -type IndexPatternState = Record; +export type IndexPatternState = Record; type LoadingState = Record; export function IndexPatternContextProvider({ children }: ProviderProps) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx new file mode 100644 index 000000000000..4f19a8131f66 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useState } from 'react'; +import { useKibana } from '../../../../utils/kibana_react'; +import { SeriesConfig, SeriesUrl } from '../types'; +import { useAppIndexPatternContext } from './use_app_index_pattern'; +import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter } from '../configurations/utils'; +import { getFiltersFromDefs } from './use_lens_attributes'; +import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; + +interface UseDiscoverLink { + seriesConfig?: SeriesConfig; + series: SeriesUrl; +} + +export const useDiscoverLink = ({ series, seriesConfig }: UseDiscoverLink) => { + const kServices = useKibana().services; + const { + application: { navigateToUrl }, + } = kServices; + + const { indexPatterns } = useAppIndexPatternContext(); + + const urlGenerator = kServices.discover?.urlGenerator; + const [discoverUrl, setDiscoverUrl] = useState(''); + + useEffect(() => { + const indexPattern = indexPatterns?.[series.dataType]; + + const definitions = series.reportDefinitions ?? {}; + const filters = [...(seriesConfig?.baseFilters ?? [])]; + + const definitionFilters = getFiltersFromDefs(definitions); + + definitionFilters.forEach(({ field, values = [] }) => { + if (values.length > 1) { + filters.push(buildPhrasesFilter(field, values, indexPattern)[0]); + } else { + filters.push(buildPhraseFilter(field, values[0], indexPattern)[0]); + } + }); + + const selectedMetricField = series.selectedMetricField; + + if ( + selectedMetricField && + selectedMetricField !== RECORDS_FIELD && + selectedMetricField !== RECORDS_PERCENTAGE_FIELD + ) { + filters.push(buildExistsFilter(selectedMetricField, indexPattern)[0]); + } + + const getDiscoverUrl = async () => { + if (!urlGenerator?.createUrl) return; + + const newUrl = await urlGenerator.createUrl({ + filters, + indexPatternId: indexPattern?.id, + }); + setDiscoverUrl(newUrl); + }; + getDiscoverUrl(); + }, [ + indexPatterns, + series.dataType, + series.reportDefinitions, + series.selectedMetricField, + seriesConfig?.baseFilters, + urlGenerator, + ]); + + const onClick = useCallback( + (event: React.MouseEvent) => { + if (discoverUrl) { + event.preventDefault(); + + return navigateToUrl(discoverUrl); + } + }, + [discoverUrl, navigateToUrl] + ); + + return { + href: discoverUrl, + onClick, + }; +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 8bb265b4f6d8..ef974d54e6cd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -9,12 +9,18 @@ import { useMemo } from 'react'; import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../../lens/public'; import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; -import { useSeriesStorage } from './use_series_storage'; +import { + AllSeries, + allSeriesKey, + convertAllShortSeries, + useSeriesStorage, +} from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_combobox'; +import { useTheme } from '../../../../hooks/use_theme'; export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => { return Object.entries(reportDefinitions ?? {}) @@ -28,41 +34,54 @@ export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitio }; export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { - const { allSeriesIds, allSeries } = useSeriesStorage(); + const { storage, allSeries, lastRefresh, reportType } = useSeriesStorage(); const { indexPatterns } = useAppIndexPatternContext(); + const theme = useTheme(); + return useMemo(() => { - if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) { + if (isEmpty(indexPatterns) || isEmpty(allSeries) || !reportType) { return null; } + const allSeriesT: AllSeries = convertAllShortSeries(storage.get(allSeriesKey) ?? []); + const layerConfigs: LayerConfig[] = []; - allSeriesIds.forEach((seriesIdT) => { - const seriesT = allSeries[seriesIdT]; - const indexPattern = indexPatterns?.[seriesT?.dataType]; - if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { + allSeriesT.forEach((series, seriesIndex) => { + const indexPattern = indexPatterns?.[series?.dataType]; + + if ( + indexPattern && + !isEmpty(series.reportDefinitions) && + !series.hidden && + series.selectedMetricField + ) { const seriesConfig = getDefaultConfigs({ - reportType: seriesT.reportType, - dataType: seriesT.dataType, + reportType, indexPattern, + dataType: series.dataType, }); - const filters: UrlFilter[] = (seriesT.filters ?? []).concat( - getFiltersFromDefs(seriesT.reportDefinitions) + const filters: UrlFilter[] = (series.filters ?? []).concat( + getFiltersFromDefs(series.reportDefinitions) ); + const color = `euiColorVis${seriesIndex}`; + layerConfigs.push({ filters, indexPattern, seriesConfig, - time: seriesT.time, - breakdown: seriesT.breakdown, - seriesType: seriesT.seriesType, - operationType: seriesT.operationType, - reportDefinitions: seriesT.reportDefinitions ?? {}, - selectedMetricField: seriesT.selectedMetricField, + time: series.time, + name: series.name, + breakdown: series.breakdown, + seriesType: series.seriesType, + operationType: series.operationType, + reportDefinitions: series.reportDefinitions ?? {}, + selectedMetricField: series.selectedMetricField, + color: series.color ?? (theme.eui as unknown as Record)[color], }); } }); @@ -73,6 +92,6 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null const lensAttributes = new LensAttributes(layerConfigs); - return lensAttributes.getJSON(); - }, [indexPatterns, allSeriesIds, allSeries]); + return lensAttributes.getJSON(lastRefresh); + }, [indexPatterns, allSeries, reportType, storage, theme, lastRefresh]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts index 2d2618bc4615..f2a6130cdc59 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -6,18 +6,16 @@ */ import { useSeriesStorage } from './use_series_storage'; -import { UrlFilter } from '../types'; +import { SeriesUrl, UrlFilter } from '../types'; export interface UpdateFilter { field: string; - value: string; + value: string | string[]; negate?: boolean; } -export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); +export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; series: SeriesUrl }) => { + const { setSeries } = useSeriesStorage(); const filters = series.filters ?? []; @@ -26,10 +24,14 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { .map((filter) => { if (filter.field === field) { if (negate) { - const notValuesN = filter.notValues?.filter((val) => val !== value); + const notValuesN = filter.notValues?.filter((val) => + value instanceof Array ? !value.includes(val) : val !== value + ); return { ...filter, notValues: notValuesN }; } else { - const valuesN = filter.values?.filter((val) => val !== value); + const valuesN = filter.values?.filter((val) => + value instanceof Array ? !value.includes(val) : val !== value + ); return { ...filter, values: valuesN }; } } @@ -43,9 +45,9 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { const addFilter = ({ field, value, negate }: UpdateFilter) => { const currFilter: UrlFilter = { field }; if (negate) { - currFilter.notValues = [value]; + currFilter.notValues = value instanceof Array ? value : [value]; } else { - currFilter.values = [value]; + currFilter.values = value instanceof Array ? value : [value]; } if (filters.length === 0) { setSeries(seriesId, { ...series, filters: [currFilter] }); @@ -65,13 +67,26 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { const currNotValues = currFilter.notValues ?? []; const currValues = currFilter.values ?? []; - const notValues = currNotValues.filter((val) => val !== value); - const values = currValues.filter((val) => val !== value); + const notValues = currNotValues.filter((val) => + value instanceof Array ? !value.includes(val) : val !== value + ); + + const values = currValues.filter((val) => + value instanceof Array ? !value.includes(val) : val !== value + ); if (negate) { - notValues.push(value); + if (value instanceof Array) { + notValues.push(...value); + } else { + notValues.push(value); + } } else { - values.push(value); + if (value instanceof Array) { + values.push(...value); + } else { + values.push(value); + } } currFilter.notValues = notValues.length > 0 ? notValues : undefined; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx index c32acc47abd1..ce6d7bd94d8e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx @@ -6,37 +6,39 @@ */ import React, { useEffect } from 'react'; - -import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; +import { Route, Router } from 'react-router-dom'; import { render } from '@testing-library/react'; +import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; +import { getHistoryFromUrl } from '../rtl_helpers'; -const mockSingleSeries = { - 'performance-distribution': { - reportType: 'data-distribution', +const mockSingleSeries = [ + { + name: 'performance-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -}; +]; -const mockMultipleSeries = { - 'performance-distribution': { - reportType: 'data-distribution', +const mockMultipleSeries = [ + { + name: 'performance-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - 'kpi-over-time': { - reportType: 'kpi-over-time', + { + name: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -}; +]; -describe('userSeries', function () { +describe('userSeriesStorage', function () { function setupTestComponent(seriesData: any) { const setData = jest.fn(); + function TestComponent() { const data = useSeriesStorage(); @@ -48,11 +50,20 @@ describe('userSeries', function () { } render( - - - + + + (key === 'sr' ? seriesData : null)), + set: jest.fn(), + }} + > + + + + ); return setData; @@ -63,22 +74,20 @@ describe('userSeries', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: { - 'performance-distribution': { - breakdown: 'user_agent.name', + allSeries: [ + { + name: 'performance-distribution', dataType: 'ux', - reportType: 'data-distribution', + breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - }, - allSeriesIds: ['performance-distribution'], + ], firstSeries: { - breakdown: 'user_agent.name', + name: 'performance-distribution', dataType: 'ux', - reportType: 'data-distribution', + breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - firstSeriesId: 'performance-distribution', }) ); }); @@ -89,42 +98,38 @@ describe('userSeries', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: { - 'performance-distribution': { - breakdown: 'user_agent.name', + allSeries: [ + { + name: 'performance-distribution', dataType: 'ux', - reportType: 'data-distribution', + breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - 'kpi-over-time': { - reportType: 'kpi-over-time', + { + name: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - }, - allSeriesIds: ['performance-distribution', 'kpi-over-time'], + ], firstSeries: { - breakdown: 'user_agent.name', + name: 'performance-distribution', dataType: 'ux', - reportType: 'data-distribution', + breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - firstSeriesId: 'performance-distribution', }) ); }); it('should return expected result when there are no series', function () { - const setData = setupTestComponent({}); + const setData = setupTestComponent([]); - expect(setData).toHaveBeenCalledTimes(2); + expect(setData).toHaveBeenCalledTimes(1); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: {}, - allSeriesIds: [], + allSeries: [], firstSeries: undefined, - firstSeriesId: undefined, }) ); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index a47a124d14b4..d9a5adc82214 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -22,13 +22,17 @@ import { OperationType, SeriesType } from '../../../../../../lens/public'; import { URL_KEYS } from '../configurations/constants/url_constants'; export interface SeriesContextValue { - firstSeries: SeriesUrl; - firstSeriesId: string; - allSeriesIds: string[]; + firstSeries?: SeriesUrl; + lastRefresh: number; + setLastRefresh: (val: number) => void; + applyChanges: () => void; allSeries: AllSeries; - setSeries: (seriesIdN: string, newValue: SeriesUrl) => void; - getSeries: (seriesId: string) => SeriesUrl; - removeSeries: (seriesId: string) => void; + setSeries: (seriesIndex: number, newValue: SeriesUrl) => void; + getSeries: (seriesIndex: number) => SeriesUrl | undefined; + removeSeries: (seriesIndex: number) => void; + setReportType: (reportType: string) => void; + storage: IKbnUrlStateStorage | ISessionStorageStateStorage; + reportType: ReportViewType; } export const UrlStorageContext = createContext({} as SeriesContextValue); @@ -36,72 +40,87 @@ interface ProviderProps { storage: IKbnUrlStateStorage | ISessionStorageStateStorage; } -function convertAllShortSeries(allShortSeries: AllShortSeries) { - const allSeriesIds = Object.keys(allShortSeries); - const allSeriesN: AllSeries = {}; - allSeriesIds.forEach((seriesKey) => { - allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); - }); - - return allSeriesN; +export function convertAllShortSeries(allShortSeries: AllShortSeries) { + return (allShortSeries ?? []).map((shortSeries) => convertFromShortUrl(shortSeries)); } +export const allSeriesKey = 'sr'; +const reportTypeKey = 'reportType'; + export function UrlStorageContextProvider({ children, storage, }: ProviderProps & { children: JSX.Element }) { - const allSeriesKey = 'sr'; - - const [allShortSeries, setAllShortSeries] = useState( - () => storage.get(allSeriesKey) ?? {} - ); const [allSeries, setAllSeries] = useState(() => - convertAllShortSeries(storage.get(allSeriesKey) ?? {}) + convertAllShortSeries(storage.get(allSeriesKey) ?? []) ); - const [firstSeriesId, setFirstSeriesId] = useState(''); + + const [lastRefresh, setLastRefresh] = useState(() => Date.now()); + + const [reportType, setReportType] = useState( + () => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '' + ); + const [firstSeries, setFirstSeries] = useState(); useEffect(() => { - const allSeriesIds = Object.keys(allShortSeries); - const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {}); + const firstSeriesT = allSeries?.[0]; - setAllSeries(allSeriesN); - setFirstSeriesId(allSeriesIds?.[0]); - setFirstSeries(allSeriesN?.[allSeriesIds?.[0]]); - (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - }, [allShortSeries, storage]); + setFirstSeries(firstSeriesT); + }, [allSeries, storage]); - const setSeries = (seriesIdN: string, newValue: SeriesUrl) => { - setAllShortSeries((prevState) => { - prevState[seriesIdN] = convertToShortUrl(newValue); - return { ...prevState }; - }); - }; + const setSeries = useCallback((seriesIndex: number, newValue: SeriesUrl) => { + setAllSeries((prevAllSeries) => { + const newStateRest = prevAllSeries.map((series, index) => { + if (index === seriesIndex) { + return newValue; + } + return series; + }); + + if (prevAllSeries.length === seriesIndex) { + return [...newStateRest, newValue]; + } - const removeSeries = (seriesIdN: string) => { - setAllShortSeries((prevState) => { - delete prevState[seriesIdN]; - return { ...prevState }; + return [...newStateRest]; }); - }; + }, []); - const allSeriesIds = Object.keys(allShortSeries); + useEffect(() => { + (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); + }, [reportType, storage]); + + const removeSeries = useCallback((seriesIndex: number) => { + setAllSeries((prevAllSeries) => + prevAllSeries.filter((seriesT, index) => index !== seriesIndex) + ); + }, []); const getSeries = useCallback( - (seriesId?: string) => { - return seriesId ? allSeries?.[seriesId] ?? {} : ({} as SeriesUrl); + (seriesIndex: number) => { + return allSeries[seriesIndex]; }, [allSeries] ); + const applyChanges = useCallback(() => { + const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); + + (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); + setLastRefresh(Date.now()); + }, [allSeries, storage]); + const value = { + applyChanges, storage, getSeries, setSeries, removeSeries, - firstSeriesId, allSeries, - allSeriesIds, + lastRefresh, + setLastRefresh, + setReportType, + reportType: storage.get(reportTypeKey) as ReportViewType, firstSeries: firstSeries!, }; return {children}; @@ -112,10 +131,9 @@ export function useSeriesStorage() { } function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { - const { dt, op, st, rt, bd, ft, time, rdf, mt, ...restSeries } = newValue; + const { dt, op, st, bd, ft, time, rdf, mt, h, n, c, ...restSeries } = newValue; return { operationType: op, - reportType: rt!, seriesType: st, breakdown: bd, filters: ft!, @@ -123,26 +141,31 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { reportDefinitions: rdf, dataType: dt!, selectedMetricField: mt, + hidden: h, + name: n, + color: c, ...restSeries, }; } interface ShortUrlSeries { [URL_KEYS.OPERATION_TYPE]?: OperationType; - [URL_KEYS.REPORT_TYPE]?: ReportViewType; [URL_KEYS.DATA_TYPE]?: AppDataType; [URL_KEYS.SERIES_TYPE]?: SeriesType; [URL_KEYS.BREAK_DOWN]?: string; [URL_KEYS.FILTERS]?: UrlFilter[]; [URL_KEYS.REPORT_DEFINITIONS]?: URLReportDefinition; [URL_KEYS.SELECTED_METRIC]?: string; + [URL_KEYS.HIDDEN]?: boolean; + [URL_KEYS.NAME]: string; + [URL_KEYS.COLOR]?: string; time?: { to: string; from: string; }; } -export type AllShortSeries = Record; -export type AllSeries = Record; +export type AllShortSeries = ShortUrlSeries[]; +export type AllSeries = SeriesUrl[]; -export const NEW_SERIES_KEY = 'new-series-key'; +export const NEW_SERIES_KEY = 'new-series'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index e55752ceb62b..3de29b02853e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -25,11 +25,9 @@ import { TypedLensByValueInput } from '../../../../../lens/public'; export function ExploratoryViewPage({ saveAttributes, - multiSeries = false, useSessionStorage = false, }: { useSessionStorage?: boolean; - multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' }); @@ -61,7 +59,7 @@ export function ExploratoryViewPage({ - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index 4cb586fe94ce..9e4d9486dc15 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -7,16 +7,51 @@ import { i18n } from '@kbn/i18n'; import React, { Dispatch, SetStateAction, useCallback } from 'react'; -import { combineTimeRanges } from './exploratory_view'; +import styled from 'styled-components'; +import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useSeriesStorage } from './hooks/use_series_storage'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ReportViewType, SeriesUrl } from './types'; +import { ReportTypes } from './configurations/constants'; interface Props { lensAttributes: TypedLensByValueInput['attributes']; setLastUpdated: Dispatch>; } +export const combineTimeRanges = ( + reportType: ReportViewType, + allSeries: SeriesUrl[], + firstSeries?: SeriesUrl +) => { + let to: string = ''; + let from: string = ''; + + if (reportType === ReportTypes.KPI) { + return firstSeries?.time; + } + + allSeries.forEach((series) => { + if ( + series.dataType && + series.selectedMetricField && + !isEmpty(series.reportDefinitions) && + series.time + ) { + const seriesTo = new Date(series.time.to); + const seriesFrom = new Date(series.time.from); + if (!to || seriesTo > new Date(to)) { + to = series.time.to; + } + if (!from || seriesFrom < new Date(from)) { + from = series.time.from; + } + } + }); + + return { to, from }; +}; export function LensEmbeddable(props: Props) { const { lensAttributes, setLastUpdated } = props; @@ -27,9 +62,11 @@ export function LensEmbeddable(props: Props) { const LensComponent = lens?.EmbeddableComponent; - const { firstSeriesId, firstSeries: series, setSeries, allSeries } = useSeriesStorage(); + const { firstSeries, setSeries, allSeries, reportType } = useSeriesStorage(); - const timeRange = combineTimeRanges(allSeries, series); + const firstSeriesId = 0; + + const timeRange = firstSeries ? combineTimeRanges(reportType, allSeries, firstSeries) : null; const onLensLoad = useCallback(() => { setLastUpdated(Date.now()); @@ -37,9 +74,9 @@ export function LensEmbeddable(props: Props) { const onBrushEnd = useCallback( ({ range }: { range: number[] }) => { - if (series?.reportType !== 'data-distribution') { + if (reportType !== 'data-distribution' && firstSeries) { setSeries(firstSeriesId, { - ...series, + ...firstSeries, time: { from: new Date(range[0]).toISOString(), to: new Date(range[1]).toISOString(), @@ -53,16 +90,30 @@ export function LensEmbeddable(props: Props) { ); } }, - [notifications?.toasts, series, firstSeriesId, setSeries] + [reportType, setSeries, firstSeries, notifications?.toasts] ); + if (timeRange === null || !firstSeries) { + return null; + } + return ( - + + + ); } + +const LensWrapper = styled.div` + height: 100%; + + &&& > div { + height: 100%; + } +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index a577a8df3e3d..48a22f91eb7f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -10,7 +10,7 @@ import React, { ReactElement } from 'react'; import { stringify } from 'query-string'; // eslint-disable-next-line import/no-extraneous-dependencies import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; -import { Router } from 'react-router-dom'; +import { Route, Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; import { CoreStart } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; @@ -24,7 +24,7 @@ import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/ import { lensPluginMock } from '../../../../../lens/public/mocks'; import * as useAppIndexPatternHook from './hooks/use_app_index_pattern'; import { IndexPatternContextProvider } from './hooks/use_app_index_pattern'; -import { AllSeries, UrlStorageContext } from './hooks/use_series_storage'; +import { AllSeries, SeriesContextValue, UrlStorageContext } from './hooks/use_series_storage'; import * as fetcherHook from '../../../hooks/use_fetcher'; import * as useSeriesFilterHook from './hooks/use_series_filters'; @@ -35,10 +35,12 @@ import indexPatternData from './configurations/test_data/test_index_pattern.json // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services'; import { IndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/common'; + +import { AppDataType, SeriesUrl, UrlFilter } from './types'; import { createStubIndexPattern } from '../../../../../../../src/plugins/data/common/stubs'; -import { AppDataType, UrlFilter } from './types'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ListItem } from '../../../hooks/use_values_list'; +import { TRANSACTION_DURATION } from './configurations/constants/elasticsearch_fieldnames'; import { casesPluginMock } from '../../../../../cases/public/mocks'; interface KibanaProps { @@ -157,9 +159,11 @@ export function MockRouter({ }: MockRouterProps) { return ( - - {children} - + + + {children} + + ); } @@ -172,7 +176,7 @@ export function render( core: customCore, kibanaProps, renderOptions, - url, + url = '/app/observability/exploratory-view/', initSeries = {}, }: RenderRouterOptions = {} ) { @@ -202,7 +206,7 @@ export function render( }; } -const getHistoryFromUrl = (url: Url) => { +export const getHistoryFromUrl = (url: Url) => { if (typeof url === 'string') { return createMemoryHistory({ initialEntries: [url], @@ -251,6 +255,15 @@ export const mockUseValuesList = (values?: ListItem[]) => { return { spy, onRefreshTimeRange }; }; +export const mockUxSeries = { + name: 'performance-distribution', + dataType: 'ux', + breakdown: 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + reportDefinitions: { 'service.name': ['elastic-co'] }, + selectedMetricField: TRANSACTION_DURATION, +} as SeriesUrl; + function mockSeriesStorageContext({ data, filters, @@ -260,34 +273,34 @@ function mockSeriesStorageContext({ filters?: UrlFilter[]; breakdown?: string; }) { - const mockDataSeries = data || { - 'performance-distribution': { - reportType: 'data-distribution', - dataType: 'ux', - breakdown: breakdown || 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - ...(filters ? { filters } : {}), - }, + const testSeries = { + ...mockUxSeries, + breakdown: breakdown || 'user_agent.name', + ...(filters ? { filters } : {}), }; - const allSeriesIds = Object.keys(mockDataSeries); - const firstSeriesId = allSeriesIds?.[0]; - const series = mockDataSeries[firstSeriesId]; + const mockDataSeries = data || [testSeries]; const removeSeries = jest.fn(); const setSeries = jest.fn(); - const getSeries = jest.fn().mockReturnValue(series); + const getSeries = jest.fn().mockReturnValue(testSeries); return { - firstSeriesId, - allSeriesIds, removeSeries, setSeries, getSeries, - firstSeries: mockDataSeries[firstSeriesId], + autoApply: true, + reportType: 'data-distribution', + lastRefresh: Date.now(), + setLastRefresh: jest.fn(), + setAutoApply: jest.fn(), + applyChanges: jest.fn(), + firstSeries: mockDataSeries[0], allSeries: mockDataSeries, - }; + setReportType: jest.fn(), + storage: { get: jest.fn().mockReturnValue(mockDataSeries) } as any, + } as SeriesContextValue; } export function mockUseSeriesFilter() { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx deleted file mode 100644 index b10702ebded5..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; -import { mockAppIndexPattern, render } from '../../rtl_helpers'; -import { dataTypes, DataTypesCol } from './data_types_col'; - -describe('DataTypesCol', function () { - const seriesId = 'test-series-id'; - - mockAppIndexPattern(); - - it('should render properly', function () { - const { getByText } = render(); - - dataTypes.forEach(({ label }) => { - getByText(label); - }); - }); - - it('should set series on change', function () { - const { setSeries } = render(); - - fireEvent.click(screen.getByText(/user experience \(rum\)/i)); - - expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { - dataType: 'ux', - isNew: true, - time: { - from: 'now-15m', - to: 'now', - }, - }); - }); - - it('should set series on change on already selected', function () { - const initSeries = { - data: { - [seriesId]: { - dataType: 'synthetics' as const, - reportType: 'kpi-over-time' as const, - breakdown: 'monitor.status', - time: { from: 'now-15m', to: 'now' }, - }, - }, - }; - - render(, { initSeries }); - - const button = screen.getByRole('button', { - name: /Synthetic Monitoring/i, - }); - - expect(button.classList).toContain('euiButton--fill'); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx deleted file mode 100644 index f386f62d9ed7..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; -import { AppDataType } from '../../types'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; - -export const dataTypes: Array<{ id: AppDataType; label: string }> = [ - { id: 'synthetics', label: 'Synthetic Monitoring' }, - { id: 'ux', label: 'User Experience (RUM)' }, - { id: 'mobile', label: 'Mobile Experience' }, - // { id: 'infra_logs', label: 'Logs' }, - // { id: 'infra_metrics', label: 'Metrics' }, - // { id: 'apm', label: 'APM' }, -]; - -export function DataTypesCol({ seriesId }: { seriesId: string }) { - const { getSeries, setSeries, removeSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - const { loading } = useAppIndexPatternContext(); - - const onDataTypeChange = (dataType?: AppDataType) => { - if (!dataType) { - removeSeries(seriesId); - } else { - setSeries(seriesId || `${dataType}-series`, { - dataType, - isNew: true, - time: series.time, - } as any); - } - }; - - const selectedDataType = series.dataType; - - return ( - - {dataTypes.map(({ id: dataTypeId, label }) => ( - - - - ))} - - ); -} - -const FlexGroup = styled(EuiFlexGroup)` - width: 100%; -`; - -const Button = styled(EuiButton)` - will-change: transform; -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx deleted file mode 100644 index 6be78084ae19..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import styled from 'styled-components'; -import { SeriesDatePicker } from '../../series_date_picker'; -import { DateRangePicker } from '../../series_date_picker/date_range_picker'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; - -interface Props { - seriesId: string; -} -export function DatePickerCol({ seriesId }: Props) { - const { firstSeriesId, getSeries } = useSeriesStorage(); - const { reportType } = getSeries(firstSeriesId); - - return ( - - {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( - - ) : ( - - )} - - ); -} - -const Wrapper = styled.div` - .euiSuperDatePicker__flexWrapper { - width: 100%; - > .euiFlexItem { - margin-right: 0px; - } - } -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx deleted file mode 100644 index a5e5ad3900de..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; -import { getDefaultConfigs } from '../../configurations/default_configs'; -import { mockIndexPattern, render } from '../../rtl_helpers'; -import { ReportBreakdowns } from './report_breakdowns'; -import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; - -describe('Series Builder ReportBreakdowns', function () { - const seriesId = 'test-series-id'; - const dataViewSeries = getDefaultConfigs({ - reportType: 'data-distribution', - dataType: 'ux', - indexPattern: mockIndexPattern, - }); - - it('should render properly', function () { - render(); - - screen.getByText('Select an option: , is selected'); - screen.getAllByText('Browser family'); - }); - - it('should set new series breakdown on change', function () { - const { setSeries } = render( - - ); - - const btn = screen.getByRole('button', { - name: /select an option: Browser family , is selected/i, - hidden: true, - }); - - fireEvent.click(btn); - - fireEvent.click(screen.getByText(/operating system/i)); - - expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { - breakdown: USER_AGENT_OS, - dataType: 'ux', - reportType: 'data-distribution', - time: { from: 'now-15m', to: 'now' }, - }); - }); - it('should set undefined on new series on no select breakdown', function () { - const { setSeries } = render( - - ); - - const btn = screen.getByRole('button', { - name: /select an option: Browser family , is selected/i, - hidden: true, - }); - - fireEvent.click(btn); - - fireEvent.click(screen.getByText(/no breakdown/i)); - - expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { - breakdown: undefined, - dataType: 'ux', - reportType: 'data-distribution', - time: { from: 'now-15m', to: 'now' }, - }); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx deleted file mode 100644 index fa2d01691ce1..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Breakdowns } from '../../series_editor/columns/breakdowns'; -import { SeriesConfig } from '../../types'; - -export function ReportBreakdowns({ - seriesId, - seriesConfig, -}: { - seriesConfig: SeriesConfig; - seriesId: string; -}) { - return ( - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx deleted file mode 100644 index 7962bf2b924f..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; -import styled from 'styled-components'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { ReportMetricOptions } from '../report_metric_options'; -import { SeriesConfig } from '../../types'; -import { SeriesChartTypesSelect } from './chart_types'; -import { OperationTypeSelect } from './operation_type_select'; -import { DatePickerCol } from './date_picker_col'; -import { parseCustomFieldName } from '../../configurations/lens_attributes'; -import { ReportDefinitionField } from './report_definition_field'; - -function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { - const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); - - return columnType; -} - -export function ReportDefinitionCol({ - seriesConfig, - seriesId, -}: { - seriesConfig: SeriesConfig; - seriesId: string; -}) { - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - - const { reportDefinitions: selectedReportDefinitions = {}, selectedMetricField } = series ?? {}; - - const { definitionFields, defaultSeriesType, hasOperationType, yAxisColumns, metricOptions } = - seriesConfig; - - const onChange = (field: string, value?: string[]) => { - if (!value?.[0]) { - delete selectedReportDefinitions[field]; - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions }, - }); - } else { - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions, [field]: value }, - }); - } - }; - - const columnType = getColumnType(seriesConfig, selectedMetricField); - - return ( - - - - - - {definitionFields.map((field) => ( - - - - ))} - {metricOptions && ( - - - - )} - {(hasOperationType || columnType === 'operation') && ( - - - - )} - - - - - ); -} - -const FlexGroup = styled(EuiFlexGroup)` - width: 100%; -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx deleted file mode 100644 index 0b183b5f20c0..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { screen } from '@testing-library/react'; -import { ReportFilters } from './report_filters'; -import { getDefaultConfigs } from '../../configurations/default_configs'; -import { mockIndexPattern, render } from '../../rtl_helpers'; - -describe('Series Builder ReportFilters', function () { - const seriesId = 'test-series-id'; - - const dataViewSeries = getDefaultConfigs({ - reportType: 'data-distribution', - indexPattern: mockIndexPattern, - dataType: 'ux', - }); - - it('should render properly', function () { - render(); - - screen.getByText('Add filter'); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx deleted file mode 100644 index d5938c5387e8..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { SeriesFilter } from '../../series_editor/columns/series_filter'; -import { SeriesConfig } from '../../types'; - -export function ReportFilters({ - seriesConfig, - seriesId, -}: { - seriesConfig: SeriesConfig; - seriesId: string; -}) { - return ( - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx deleted file mode 100644 index 12ae8560453c..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; -import { mockAppIndexPattern, render } from '../../rtl_helpers'; -import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; -import { ReportTypes } from '../series_builder'; -import { DEFAULT_TIME } from '../../configurations/constants'; - -describe('ReportTypesCol', function () { - const seriesId = 'performance-distribution'; - - mockAppIndexPattern(); - - it('should render properly', function () { - render(); - screen.getByText('Performance distribution'); - screen.getByText('KPI over time'); - }); - - it('should display empty message', function () { - render(); - screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT); - }); - - it('should set series on change', function () { - const { setSeries } = render( - - ); - - fireEvent.click(screen.getByText(/KPI over time/i)); - - expect(setSeries).toHaveBeenCalledWith(seriesId, { - dataType: 'ux', - selectedMetricField: undefined, - reportType: 'kpi-over-time', - time: { from: 'now-15m', to: 'now' }, - }); - expect(setSeries).toHaveBeenCalledTimes(1); - }); - - it('should set selected as filled', function () { - const initSeries = { - data: { - [seriesId]: { - dataType: 'synthetics' as const, - reportType: 'kpi-over-time' as const, - breakdown: 'monitor.status', - time: { from: 'now-15m', to: 'now' }, - isNew: true, - }, - }, - }; - - const { setSeries } = render( - , - { initSeries } - ); - - const button = screen.getByRole('button', { - name: /KPI over time/i, - }); - - expect(button.classList).toContain('euiButton--fill'); - fireEvent.click(button); - - // undefined on click selected - expect(setSeries).toHaveBeenCalledWith(seriesId, { - dataType: 'synthetics', - time: DEFAULT_TIME, - isNew: true, - }); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx deleted file mode 100644 index c4eebbfaca3e..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { map } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import styled from 'styled-components'; -import { ReportViewType, SeriesUrl } from '../../types'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { DEFAULT_TIME } from '../../configurations/constants'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { ReportTypeItem } from '../series_builder'; - -interface Props { - seriesId: string; - reportTypes: ReportTypeItem[]; -} - -export function ReportTypesCol({ seriesId, reportTypes }: Props) { - const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage(); - - const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId); - - const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); - - if (!restSeries.dataType) { - return ( - - ); - } - - if (!loading && !hasData) { - return ( - - ); - } - - const disabledReportTypes: ReportViewType[] = map( - reportTypes.filter( - ({ reportType }) => firstSeriesId !== seriesId && reportType !== firstSeries.reportType - ), - 'reportType' - ); - - return reportTypes?.length > 0 ? ( - - {reportTypes.map(({ reportType, label }) => ( - - - - ))} - - ) : ( - {SELECTED_DATA_TYPE_FOR_REPORT} - ); -} - -export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate( - 'xpack.observability.expView.reportType.noDataType', - { defaultMessage: 'No data type selected.' } -); - -const FlexGroup = styled(EuiFlexGroup)` - width: 100%; -`; - -const Button = styled(EuiButton)` - will-change: transform; -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx deleted file mode 100644 index a2a3e34c2183..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiSuperSelect } from '@elastic/eui'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { SeriesConfig } from '../types'; - -interface Props { - seriesId: string; - defaultValue?: string; - options: SeriesConfig['metricOptions']; -} - -export function ReportMetricOptions({ seriesId, options: opts }: Props) { - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - - const onChange = (value: string) => { - setSeries(seriesId, { - ...series, - selectedMetricField: value, - }); - }; - - const options = opts ?? []; - - return ( - ({ - value: fd || id, - inputDisplay: label, - }))} - valueOfSelected={series.selectedMetricField || options?.[0].field || options?.[0].id} - onChange={(value) => onChange(value)} - /> - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx deleted file mode 100644 index 684cf3a210a5..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { RefObject, useEffect, useState } from 'react'; -import { isEmpty } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { - EuiBasicTable, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiSwitch, -} from '@elastic/eui'; -import { rgba } from 'polished'; -import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; -import { DataTypesCol } from './columns/data_types_col'; -import { ReportTypesCol } from './columns/report_types_col'; -import { ReportDefinitionCol } from './columns/report_definition_col'; -import { ReportFilters } from './columns/report_filters'; -import { ReportBreakdowns } from './columns/report_breakdowns'; -import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { getDefaultConfigs } from '../configurations/default_configs'; -import { SeriesEditor } from '../series_editor/series_editor'; -import { SeriesActions } from '../series_editor/columns/series_actions'; -import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; -import { LastUpdated } from './last_updated'; -import { - CORE_WEB_VITALS_LABEL, - DEVICE_DISTRIBUTION_LABEL, - KPI_OVER_TIME_LABEL, - PERF_DIST_LABEL, -} from '../configurations/constants/labels'; - -export interface ReportTypeItem { - id: string; - reportType: ReportViewType; - label: string; -} - -export const ReportTypes: Record = { - synthetics: [ - { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, - { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, - ], - ux: [ - { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, - { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, - { id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, - ], - mobile: [ - { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, - { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, - { id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, - ], - apm: [], - infra_logs: [], - infra_metrics: [], -}; - -interface BuilderItem { - id: string; - series: SeriesUrl; - seriesConfig?: SeriesConfig; -} - -export function SeriesBuilder({ - seriesBuilderRef, - lastUpdated, - multiSeries, -}: { - seriesBuilderRef: RefObject; - lastUpdated?: number; - multiSeries?: boolean; -}) { - const [editorItems, setEditorItems] = useState([]); - const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage(); - - const { loading, indexPatterns } = useAppIndexPatternContext(); - - useEffect(() => { - const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => { - if (indexPatterns?.[dataType]) { - return getDefaultConfigs({ - dataType, - indexPattern: indexPatterns[dataType], - reportType: reportType!, - }); - } - }; - - const seriesToEdit: BuilderItem[] = - allSeriesIds - .filter((sId) => { - return allSeries?.[sId]?.isNew; - }) - .map((sId) => { - const series = getSeries(sId); - const seriesConfig = getDataViewSeries(series.dataType, series.reportType); - - return { id: sId, series, seriesConfig }; - }) ?? []; - const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }]; - setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries); - }, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]); - - const columns = [ - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', { - defaultMessage: 'Data Type', - }), - field: 'id', - width: '15%', - render: (seriesId: string) => , - }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.report', { - defaultMessage: 'Report', - }), - width: '15%', - field: 'id', - render: (seriesId: string, { series: { dataType } }: BuilderItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', { - defaultMessage: 'Definition', - }), - width: '30%', - field: 'id', - render: ( - seriesId: string, - { series: { dataType, reportType }, seriesConfig }: BuilderItem - ) => { - if (dataType && seriesConfig) { - return loading ? ( - LOADING_VIEW - ) : reportType ? ( - - ) : ( - SELECT_REPORT_TYPE - ); - } - - return null; - }, - }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', { - defaultMessage: 'Filters', - }), - width: '20%', - field: 'id', - render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => - reportType && seriesConfig ? ( - - ) : null, - }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', { - defaultMessage: 'Breakdowns', - }), - width: '20%', - field: 'id', - render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => - reportType && seriesConfig ? ( - - ) : null, - }, - ...(multiSeries - ? [ - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { - defaultMessage: 'Actions', - }), - align: 'center' as const, - width: '10%', - field: 'id', - render: (seriesId: string, item: BuilderItem) => ( - - ), - }, - ] - : []), - ]; - - const applySeries = () => { - editorItems.forEach(({ series, id: seriesId }) => { - const { reportType, reportDefinitions, isNew, ...restSeries } = series; - - if (reportType && !isEmpty(reportDefinitions)) { - const reportDefId = Object.values(reportDefinitions ?? {})[0]; - const newSeriesId = `${reportDefId}-${reportType}`; - - const newSeriesN: SeriesUrl = { - ...restSeries, - reportType, - reportDefinitions, - }; - - setSeries(newSeriesId, newSeriesN); - removeSeries(seriesId); - } - }); - }; - - const addSeries = () => { - const prevSeries = allSeries?.[allSeriesIds?.[0]]; - setSeries( - `${NEW_SERIES_KEY}-${editorItems.length + 1}`, - prevSeries - ? ({ isNew: true, time: prevSeries.time } as SeriesUrl) - : ({ isNew: true } as SeriesUrl) - ); - }; - - return ( - - {multiSeries && ( - - - - - - {}} - compressed - /> - - - applySeries()} isDisabled={true} size="s"> - {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { - defaultMessage: 'Apply changes', - })} - - - - addSeries()} size="s"> - {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { - defaultMessage: 'Add Series', - })} - - - - )} -
- {multiSeries && } - {editorItems.length > 0 && ( - - )} - -
-
- ); -} - -const Wrapper = euiStyled.div` - max-height: 50vh; - overflow-y: scroll; - overflow-x: clip; - &::-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; - } -`; - -export const LOADING_VIEW = i18n.translate( - 'xpack.observability.expView.seriesBuilder.loadingView', - { - defaultMessage: 'Loading view ...', - } -); - -export const SELECT_REPORT_TYPE = i18n.translate( - 'xpack.observability.expView.seriesBuilder.selectReportType', - { - defaultMessage: 'No report type selected', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx deleted file mode 100644 index 207a53e13f1a..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { Breakdowns } from './columns/breakdowns'; -import { SeriesConfig } from '../types'; -import { ChartOptions } from './columns/chart_options'; - -interface Props { - seriesConfig: SeriesConfig; - seriesId: string; - breakdownFields: string[]; -} -export function ChartEditOptions({ seriesConfig, seriesId, breakdownFields }: Props) { - return ( - - - - - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index 84568e1c5068..21b766227a56 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { Breakdowns } from './breakdowns'; -import { mockIndexPattern, render } from '../../rtl_helpers'; +import { mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; @@ -20,13 +20,7 @@ describe('Breakdowns', function () { }); it('should render properly', async function () { - render( - - ); + render(); screen.getAllByText('Browser family'); }); @@ -36,9 +30,9 @@ describe('Breakdowns', function () { const { setSeries } = render( , { initSeries } ); @@ -49,10 +43,14 @@ describe('Breakdowns', function () { fireEvent.click(screen.getByText('Browser family')); - expect(setSeries).toHaveBeenCalledWith('series-id', { + expect(setSeries).toHaveBeenCalledWith(0, { breakdown: 'user_agent.name', dataType: 'ux', - reportType: 'data-distribution', + name: 'performance-distribution', + reportDefinitions: { + 'service.name': ['elastic-co'], + }, + selectedMetricField: 'transaction.duration.us', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx index 2237935d466a..6003ddbf0290 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -10,18 +10,16 @@ import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; -import { SeriesConfig } from '../../types'; +import { SeriesConfig, SeriesUrl } from '../../types'; interface Props { - seriesId: string; - breakdowns: string[]; - seriesConfig: SeriesConfig; + seriesId: number; + series: SeriesUrl; + seriesConfig?: SeriesConfig; } -export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { - const { setSeries, getSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); +export function Breakdowns({ seriesConfig, seriesId, series }: Props) { + const { setSeries } = useSeriesStorage(); const selectedBreakdown = series.breakdown; const NO_BREAKDOWN = 'no_breakdown'; @@ -40,9 +38,13 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { } }; + if (!seriesConfig) { + return null; + } + const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; - const items = breakdowns.map((breakdown) => ({ + const items = seriesConfig.breakdownFields.map((breakdown) => ({ id: breakdown, label: seriesConfig.labels[breakdown], })); @@ -50,14 +52,12 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { if (!hasUseBreakdownColumn) { items.push({ id: NO_BREAKDOWN, - label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', { - defaultMessage: 'No breakdown', - }), + label: NO_BREAK_DOWN_LABEL, }); } const options = items.map(({ id, label }) => ({ - inputDisplay: id === NO_BREAKDOWN ? label : {label}, + inputDisplay: label, value: id, dropdownDisplay: label, })); @@ -66,15 +66,18 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { selectedBreakdown || (hasUseBreakdownColumn ? options[0].value : NO_BREAKDOWN); return ( -
- onOptionChange(value)} - data-test-subj={'seriesBreakdown'} - /> -
+ onOptionChange(value)} + data-test-subj={'seriesBreakdown'} + /> ); } + +export const NO_BREAK_DOWN_LABEL = i18n.translate( + 'xpack.observability.exp.breakDownFilter.noBreakdown', + { + defaultMessage: 'No breakdown', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx deleted file mode 100644 index f2a6377fd9b7..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { SeriesConfig } from '../../types'; -import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select'; -import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types'; - -interface Props { - seriesConfig: SeriesConfig; - seriesId: string; -} - -export function ChartOptions({ seriesConfig, seriesId }: Props) { - return ( - - - - - {seriesConfig.hasOperationType && ( - - - - )} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_type_select.tsx new file mode 100644 index 000000000000..6f88de5cc2af --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_type_select.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiPopover, EuiToolTip, EuiButtonEmpty, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; +import { SeriesUrl, useFetcher } from '../../../../../index'; +import { SeriesConfig } from '../../types'; +import { SeriesChartTypesSelect } from './chart_types'; + +interface Props { + seriesId: number; + series: SeriesUrl; + seriesConfig: SeriesConfig; +} + +export function SeriesChartTypes({ seriesId, series, seriesConfig }: Props) { + const seriesType = series?.seriesType ?? seriesConfig.defaultSeriesType; + + const { + services: { lens }, + } = useKibana(); + + const { data = [] } = useFetcher(() => lens.getXyVisTypes(), [lens]); + + const icon = (data ?? []).find(({ id }) => id === seriesType)?.icon; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + setIsPopoverOpen(false)} + button={ + + setIsPopoverOpen((prevState) => !prevState)} + flush="both" + > + {icon && ( + id === seriesType)?.icon!} size="l" /> + )} + + + } + > + + + ); +} + +const EDIT_CHART_TYPE_LABEL = i18n.translate( + 'xpack.observability.expView.seriesEditor.editChartSeriesLabel', + { + defaultMessage: 'Edit chart type for series', + } +); + +const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.label', { + defaultMessage: 'Chart type', +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx similarity index 85% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx index c054853d9c87..8f196b8a05dd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { render } from '../../rtl_helpers'; +import { mockUxSeries, render } from '../../rtl_helpers'; import { SeriesChartTypesSelect, XYChartTypesSelect } from './chart_types'; describe.skip('SeriesChartTypesSelect', function () { it('should render properly', async function () { - render(); + render(); await waitFor(() => { screen.getByText(/chart type/i); @@ -21,7 +21,7 @@ describe.skip('SeriesChartTypesSelect', function () { it('should call set series on change', async function () { const { setSeries } = render( - + ); await waitFor(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx similarity index 77% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx index 50c2f91e6067..27d846502dbe 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx @@ -6,11 +6,11 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon, EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; -import { useFetcher } from '../../../../..'; +import { SeriesUrl, useFetcher } from '../../../../..'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { SeriesType } from '../../../../../../../lens/public'; @@ -20,16 +20,14 @@ const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes. export function SeriesChartTypesSelect({ seriesId, - seriesTypes, + series, defaultChartType, }: { - seriesId: string; - seriesTypes?: SeriesType[]; + seriesId: number; + series: SeriesUrl; defaultChartType: SeriesType; }) { - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); + const { setSeries } = useSeriesStorage(); const seriesType = series?.seriesType ?? defaultChartType; @@ -42,17 +40,15 @@ export function SeriesChartTypesSelect({ onChange={onChange} value={seriesType} excludeChartTypes={['bar_percentage_stacked']} - includeChartTypes={ - seriesTypes || [ - 'bar', - 'bar_horizontal', - 'line', - 'area', - 'bar_stacked', - 'area_stacked', - 'bar_horizontal_percentage_stacked', - ] - } + includeChartTypes={[ + 'bar', + 'bar_horizontal', + 'line', + 'area', + 'bar_stacked', + 'area_stacked', + 'bar_horizontal_percentage_stacked', + ]} label={CHART_TYPE_LABEL} /> ); @@ -105,14 +101,14 @@ export function XYChartTypesSelect({ }); return ( - + + + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx new file mode 100644 index 000000000000..fc96ad0741ec --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { mockAppIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; +import { DataTypesLabels, DataTypesSelect } from './data_type_select'; +import { DataTypes } from '../../configurations/constants'; + +describe('DataTypeSelect', function () { + const seriesId = 0; + + mockAppIndexPattern(); + + it('should render properly', function () { + render(); + }); + + it('should set series on change', async function () { + const seriesWithoutDataType = { + ...mockUxSeries, + dataType: undefined, + }; + const { setSeries } = render( + + ); + + fireEvent.click(await screen.findByText('Select data type')); + fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.SYNTHETICS])); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'synthetics', + name: 'synthetics-series-1', + time: { + from: 'now-15m', + to: 'now', + }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx new file mode 100644 index 000000000000..71fd147e8e26 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { + EuiButton, + EuiPopover, + EuiListGroup, + EuiListGroupItem, + EuiBadge, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { AppDataType, SeriesUrl } from '../../types'; +import { DataTypes, ReportTypes } from '../../configurations/constants'; + +interface Props { + seriesId: number; + series: Omit & { + dataType?: SeriesUrl['dataType']; + }; +} + +export const DataTypesLabels = { + [DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', { + defaultMessage: 'User experience (RUM)', + }), + + [DataTypes.SYNTHETICS]: i18n.translate( + 'xpack.observability.overview.exploratoryView.syntheticsLabel', + { + defaultMessage: 'Synthetics monitoring', + } + ), + + [DataTypes.MOBILE]: i18n.translate( + 'xpack.observability.overview.exploratoryView.mobileExperienceLabel', + { + defaultMessage: 'Mobile experience', + } + ), +}; + +export const dataTypes: Array<{ id: AppDataType; label: string }> = [ + { + id: DataTypes.SYNTHETICS, + label: DataTypesLabels[DataTypes.SYNTHETICS], + }, + { + id: DataTypes.UX, + label: DataTypesLabels[DataTypes.UX], + }, + { + id: DataTypes.MOBILE, + label: DataTypesLabels[DataTypes.MOBILE], + }, +]; + +const SELECT_DATA_TYPE = 'SELECT_DATA_TYPE'; + +export function DataTypesSelect({ seriesId, series }: Props) { + const { setSeries, reportType } = useSeriesStorage(); + const [showOptions, setShowOptions] = useState(false); + + const onDataTypeChange = (dataType: AppDataType) => { + if (String(dataType) !== SELECT_DATA_TYPE) { + setSeries(seriesId, { + dataType, + time: series.time, + name: `${dataType}-series-${seriesId + 1}`, + }); + } + }; + + const options = dataTypes + .filter(({ id }) => { + if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { + return id === DataTypes.MOBILE; + } + if (reportType === ReportTypes.CORE_WEB_VITAL) { + return id === DataTypes.UX; + } + return true; + }) + .map(({ id, label }) => ({ + value: id, + inputDisplay: label, + })); + + return ( + <> + {!series.dataType && ( + setShowOptions((prevState) => !prevState)} + fill + size="s" + > + {SELECT_DATA_TYPE_LABEL} + + } + isOpen={showOptions} + closePopover={() => setShowOptions((prevState) => !prevState)} + > + + {options.map((option) => ( + onDataTypeChange(option.value)} + label={option.inputDisplay} + /> + ))} + + + )} + {series.dataType && ( + + {DataTypesLabels[series.dataType as DataTypes]} + + )} + + ); +} + +const SELECT_DATA_TYPE_LABEL = i18n.translate( + 'xpack.observability.overview.exploratoryView.selectDataType', + { + defaultMessage: 'Select data type', + } +); + +const SELECT_DATA_TYPE_TOOLTIP = i18n.translate( + 'xpack.observability.overview.exploratoryView.selectDataTypeTooltip', + { + defaultMessage: 'Data type cannot be edited.', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx index 41e83f407af2..b01010e4b81f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx @@ -6,24 +6,80 @@ */ import React from 'react'; -import { SeriesDatePicker } from '../../series_date_picker'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { DateRangePicker } from '../../series_date_picker/date_range_picker'; +import { DateRangePicker } from '../../components/date_range_picker'; +import { SeriesDatePicker } from '../../components/series_date_picker'; +import { AppDataType, SeriesUrl } from '../../types'; +import { ReportTypes } from '../../configurations/constants'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { SyntheticsAddData } from '../../../add_data_buttons/synthetics_add_data'; +import { MobileAddData } from '../../../add_data_buttons/mobile_add_data'; +import { UXAddData } from '../../../add_data_buttons/ux_add_data'; interface Props { - seriesId: string; + seriesId: number; + series: SeriesUrl; } -export function DatePickerCol({ seriesId }: Props) { - const { firstSeriesId, getSeries } = useSeriesStorage(); - const { reportType } = getSeries(firstSeriesId); + +const AddDataComponents: Record = { + mobile: MobileAddData, + ux: UXAddData, + synthetics: SyntheticsAddData, + apm: null, + infra_logs: null, + infra_metrics: null, +}; + +export function DatePickerCol({ seriesId, series }: Props) { + const { reportType } = useSeriesStorage(); + + const { hasAppData } = useAppIndexPatternContext(); + + if (!series.dataType) { + return null; + } + + const AddDataButton = AddDataComponents[series.dataType]; + if (hasAppData[series.dataType] === false && AddDataButton !== null) { + return ( + + + + {i18n.translate('xpack.observability.overview.exploratoryView.noDataAvailable', { + defaultMessage: 'No {dataType} data available.', + values: { + dataType: series.dataType, + }, + })} + + + + + + + ); + } return ( -
- {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( - + + {seriesId === 0 || reportType !== ReportTypes.KPI ? ( + ) : ( - + )} -
+ ); } + +const Wrapper = styled.div` + width: 100%; + .euiSuperDatePicker__flexWrapper { + width: 100%; + > .euiFlexItem { + margin-right: 0; + } + } +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx index 90a039f6b44d..a88e2eadd10c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -8,20 +8,24 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { FilterExpanded } from './filter_expanded'; -import { mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockUxSeries, mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterExpanded', function () { - it('should render properly', async function () { - const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; + + const mockSeries = { ...mockUxSeries, filters }; + + it('render', async () => { + const initSeries = { filters }; mockAppIndexPattern(); render( , { initSeries } @@ -33,15 +37,14 @@ describe('FilterExpanded', function () { }); it('should call go back on click', async function () { - const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; - const goBack = jest.fn(); + const initSeries = { filters }; render( , { initSeries } @@ -49,28 +52,23 @@ describe('FilterExpanded', function () { await waitFor(() => { fireEvent.click(screen.getByText('Browser Family')); - - expect(goBack).toHaveBeenCalledTimes(1); - expect(goBack).toHaveBeenCalledWith(); }); }); - it('should call useValuesList on load', async function () { - const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + it('calls useValuesList on load', async () => { + const initSeries = { filters }; const { spy } = mockUseValuesList([ { label: 'Chrome', count: 10 }, { label: 'Firefox', count: 5 }, ]); - const goBack = jest.fn(); - render( , { initSeries } @@ -87,8 +85,8 @@ describe('FilterExpanded', function () { }); }); - it('should filter display values', async function () { - const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + it('filters display values', async () => { + const initSeries = { filters }; mockUseValuesList([ { label: 'Chrome', count: 10 }, @@ -97,18 +95,20 @@ describe('FilterExpanded', function () { render( , { initSeries } ); - expect(screen.getByText('Firefox')).toBeTruthy(); - await waitFor(() => { + fireEvent.click(screen.getByText('Browser Family')); + + expect(screen.queryByText('Firefox')).toBeTruthy(); + fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } }); expect(screen.queryByText('Firefox')).toBeFalsy(); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 84c326f62f89..693b79c6dc83 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -6,7 +6,14 @@ */ import React, { useState, Fragment } from 'react'; -import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup, EuiText } from '@elastic/eui'; +import { + EuiFieldSearch, + EuiSpacer, + EuiFilterGroup, + EuiText, + EuiPopover, + EuiFilterButton, +} from '@elastic/eui'; import styled from 'styled-components'; import { rgba } from 'polished'; import { i18n } from '@kbn/i18n'; @@ -14,8 +21,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { map } from 'lodash'; import { ExistsFilter, isExistsFilter } from '@kbn/es-query'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesConfig, UrlFilter } from '../../types'; +import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; @@ -23,31 +29,33 @@ import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearc import { PersistableFilter } from '../../../../../../../lens/common'; interface Props { - seriesId: string; + seriesId: number; + series: SeriesUrl; label: string; field: string; isNegated?: boolean; - goBack: () => void; nestedField?: string; filters: SeriesConfig['baseFilters']; } +export interface NestedFilterOpen { + value: string; + negate: boolean; +} + export function FilterExpanded({ seriesId, + series, field, label, - goBack, nestedField, isNegated, filters: defaultFilters, }: Props) { const [value, setValue] = useState(''); - const [isOpen, setIsOpen] = useState({ value: '', negate: false }); - - const { getSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); + const [isOpen, setIsOpen] = useState(false); + const [isNestedOpen, setIsNestedOpen] = useState({ value: '', negate: false }); const queryFilters: ESFilter[] = []; @@ -80,62 +88,71 @@ export function FilterExpanded({ ); return ( - - goBack()}> - {label} - - { - setValue(evt.target.value); - }} - placeholder={i18n.translate('xpack.observability.filters.expanded.search', { - defaultMessage: 'Search for {label}', - values: { label }, - })} - /> - - - {displayValues.length === 0 && !loading && ( - - {i18n.translate('xpack.observability.filters.expanded.noFilter', { - defaultMessage: 'No filters found.', - })} - - )} - {displayValues.map((opt) => ( - - - {isNegated !== false && ( + setIsOpen((prevState) => !prevState)} iconType="arrowDown"> + {label} + + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + > + + { + setValue(evt.target.value); + }} + placeholder={i18n.translate('xpack.observability.filters.expanded.search', { + defaultMessage: 'Search for {label}', + values: { label }, + })} + /> + + + {displayValues.length === 0 && !loading && ( + + {i18n.translate('xpack.observability.filters.expanded.noFilter', { + defaultMessage: 'No filters found.', + })} + + )} + {displayValues.map((opt) => ( + + + {isNegated !== false && ( + + )} - )} - - - - - ))} - - + + + + ))} + + + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx index a9609abc70d6..764a27fd663f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { FilterValueButton } from './filter_value_btn'; -import { mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockUxSeries, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME, USER_AGENT_VERSION, @@ -19,84 +19,98 @@ describe('FilterValueButton', function () { render( ); - screen.getByText('Chrome'); + await waitFor(() => { + expect(screen.getByText('Chrome')).toBeInTheDocument(); + }); }); - it('should render display negate state', async function () { - render( - - ); + describe('when negate is true', () => { + it('displays negate stats', async () => { + render( + + ); - await waitFor(() => { - screen.getByText('Not Chrome'); - screen.getByTitle('Not Chrome'); - const btn = screen.getByRole('button'); - expect(btn.classList).toContain('euiButtonEmpty--danger'); + await waitFor(() => { + expect(screen.getByText('Not Chrome')).toBeInTheDocument(); + expect(screen.getByTitle('Not Chrome')).toBeInTheDocument(); + const btn = screen.getByRole('button'); + expect(btn.classList).toContain('euiButtonEmpty--danger'); + }); }); - }); - it('should call set filter on click', async function () { - const { setFilter, removeFilter } = mockUseSeriesFilter(); + it('calls setFilter on click', async () => { + const { setFilter, removeFilter } = mockUseSeriesFilter(); - render( - - ); + render( + + ); - await waitFor(() => { fireEvent.click(screen.getByText('Not Chrome')); - expect(removeFilter).toHaveBeenCalledTimes(0); - expect(setFilter).toHaveBeenCalledTimes(1); - expect(setFilter).toHaveBeenCalledWith({ - field: 'user_agent.name', - negate: true, - value: 'Chrome', + + await waitFor(() => { + expect(removeFilter).toHaveBeenCalledTimes(0); + expect(setFilter).toHaveBeenCalledTimes(1); + + expect(setFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: true, + value: 'Chrome', + }); }); }); }); - it('should remove filter on click if already selected', async function () { - const { removeFilter } = mockUseSeriesFilter(); + describe('when selected', () => { + it('removes the filter on click', async () => { + const { removeFilter } = mockUseSeriesFilter(); + + render( + + ); - render( - - ); - await waitFor(() => { fireEvent.click(screen.getByText('Chrome')); - expect(removeFilter).toHaveBeenCalledWith({ - field: 'user_agent.name', - negate: false, - value: 'Chrome', + + await waitFor(() => { + expect(removeFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: false, + value: 'Chrome', + }); }); }); }); @@ -107,12 +121,13 @@ describe('FilterValueButton', function () { render( ); @@ -134,13 +149,14 @@ describe('FilterValueButton', function () { render( ); @@ -167,13 +183,14 @@ describe('FilterValueButton', function () { render( ); @@ -203,13 +220,14 @@ describe('FilterValueButton', function () { render( ); @@ -229,13 +247,14 @@ describe('FilterValueButton', function () { render( ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx index bf4ca6eb83d9..11f29c0233ef 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -5,13 +5,15 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; + import React, { useMemo } from 'react'; import { EuiFilterButton, hexToRgb } from '@elastic/eui'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useSeriesFilters } from '../../hooks/use_series_filters'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import FieldValueSuggestions from '../../../field_value_suggestions'; +import { SeriesUrl } from '../../types'; +import { NestedFilterOpen } from './filter_expanded'; interface Props { value: string; @@ -19,12 +21,13 @@ interface Props { allSelectedValues?: string[]; negate: boolean; nestedField?: string; - seriesId: string; + seriesId: number; + series: SeriesUrl; isNestedOpen: { value: string; negate: boolean; }; - setIsNestedOpen: (val: { value: string; negate: boolean }) => void; + setIsNestedOpen: (val: NestedFilterOpen) => void; } export function FilterValueButton({ @@ -34,16 +37,13 @@ export function FilterValueButton({ field, negate, seriesId, + series, nestedField, allSelectedValues, }: Props) { - const { getSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - const { indexPatterns } = useAppIndexPatternContext(series.dataType); - const { setFilter, removeFilter } = useSeriesFilters({ seriesId }); + const { setFilter, removeFilter } = useSeriesFilters({ seriesId, series }); const hasActiveFilters = (allSelectedValues ?? []).includes(value); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx new file mode 100644 index 000000000000..4e1c38592190 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { isEmpty } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { EuiBadge } from '@elastic/eui'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { SeriesConfig, SeriesUrl } from '../../types'; + +interface Props { + series: SeriesUrl; + seriesConfig?: SeriesConfig; +} + +export function IncompleteBadge({ seriesConfig, series }: Props) { + const { loading } = useAppIndexPatternContext(); + + if (!seriesConfig) { + return null; + } + const { dataType, reportDefinitions, selectedMetricField } = series; + const { definitionFields, labels } = seriesConfig; + const isIncomplete = + (!dataType || isEmpty(reportDefinitions) || !selectedMetricField) && !loading; + + const incompleteDefinition = isEmpty(reportDefinitions) + ? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', { + defaultMessage: 'Missing {reportDefinition}', + values: { reportDefinition: labels?.[definitionFields[0]] }, + }) + : ''; + + let incompleteMessage = !selectedMetricField ? MISSING_REPORT_METRIC_LABEL : incompleteDefinition; + + if (!dataType) { + incompleteMessage = MISSING_DATA_TYPE_LABEL; + } + + if (!isIncomplete) { + return null; + } + + return {incompleteMessage}; +} + +const MISSING_REPORT_METRIC_LABEL = i18n.translate( + 'xpack.observability.overview.exploratoryView.missingReportMetric', + { + defaultMessage: 'Missing report metric', + } +); + +const MISSING_DATA_TYPE_LABEL = i18n.translate( + 'xpack.observability.overview.exploratoryView.missingDataType', + { + defaultMessage: 'Missing data type', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx index 516f04e3812b..ced4d3af057f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx @@ -7,62 +7,66 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; -import { render } from '../../rtl_helpers'; +import { mockUxSeries, render } from '../../rtl_helpers'; import { OperationTypeSelect } from './operation_type_select'; describe('OperationTypeSelect', function () { it('should render properly', function () { - render(); + render(); screen.getByText('Select an option: , is selected'); }); it('should display selected value', function () { const initSeries = { - data: { - 'performance-distribution': { + data: [ + { + name: 'performance-distribution', dataType: 'ux' as const, - reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - }, + ], }; - render(, { initSeries }); + render(, { + initSeries, + }); screen.getByText('Median'); }); it('should call set series on change', function () { const initSeries = { - data: { - 'series-id': { + data: [ + { + name: 'performance-distribution', dataType: 'ux' as const, - reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - }, + ], }; - const { setSeries } = render(, { initSeries }); + const { setSeries } = render(, { + initSeries, + }); fireEvent.click(screen.getByTestId('operationTypeSelect')); - expect(setSeries).toHaveBeenCalledWith('series-id', { + expect(setSeries).toHaveBeenCalledWith(0, { operationType: 'median', dataType: 'ux', - reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, + name: 'performance-distribution', }); fireEvent.click(screen.getByText('95th Percentile')); - expect(setSeries).toHaveBeenCalledWith('series-id', { + expect(setSeries).toHaveBeenCalledWith(0, { operationType: '95th', dataType: 'ux', - reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, + name: 'performance-distribution', }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx similarity index 91% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx index fce1383f30f3..4c10c9311704 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx @@ -11,17 +11,18 @@ import { EuiSuperSelect } from '@elastic/eui'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { OperationType } from '../../../../../../../lens/public'; +import { SeriesUrl } from '../../types'; export function OperationTypeSelect({ seriesId, + series, defaultOperationType, }: { - seriesId: string; + seriesId: number; + series: SeriesUrl; defaultOperationType?: OperationType; }) { - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); + const { setSeries } = useSeriesStorage(); const operationType = series?.operationType; @@ -83,11 +84,7 @@ export function OperationTypeSelect({ return ( { removeSeries(seriesId); }; + + const isDisabled = seriesId === 0 && allSeries.length > 1; + return ( - + + + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx similarity index 65% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx index 3d156e0ee9c2..544a294e021e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx @@ -12,14 +12,14 @@ import { mockAppIndexPattern, mockIndexPattern, mockUseValuesList, + mockUxSeries, render, } from '../../rtl_helpers'; import { ReportDefinitionCol } from './report_definition_col'; -import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportDefinitionCol', function () { mockAppIndexPattern(); - const seriesId = 'test-series-id'; + const seriesId = 0; const seriesConfig = getDefaultConfigs({ reportType: 'data-distribution', @@ -27,36 +27,24 @@ describe('Series Builder ReportDefinitionCol', function () { dataType: 'ux', }); - const initSeries = { - data: { - [seriesId]: { - dataType: 'ux' as const, - reportType: 'data-distribution' as const, - time: { from: 'now-30d', to: 'now' }, - reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, - }, - }, - }; - mockUseValuesList([{ label: 'elastic-co', count: 10 }]); - it('should render properly', async function () { - render(, { - initSeries, - }); + it('renders', async () => { + render( + + ); await waitFor(() => { - screen.getByText('Web Application'); - screen.getByText('Environment'); - screen.getByText('Select an option: Page load time, is selected'); - screen.getByText('Page load time'); + expect(screen.getByText('Web Application')).toBeInTheDocument(); + expect(screen.getByText('Environment')).toBeInTheDocument(); + expect(screen.getByText('Search Environment')).toBeInTheDocument(); }); }); it('should render selected report definitions', async function () { - render(, { - initSeries, - }); + render( + + ); expect(await screen.findByText('elastic-co')).toBeInTheDocument(); @@ -65,8 +53,7 @@ describe('Series Builder ReportDefinitionCol', function () { it('should be able to remove selected definition', async function () { const { setSeries } = render( - , - { initSeries } + ); expect( @@ -80,11 +67,14 @@ describe('Series Builder ReportDefinitionCol', function () { fireEvent.click(removeBtn); expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux', + name: 'performance-distribution', + breakdown: 'user_agent.name', reportDefinitions: {}, - reportType: 'data-distribution', - time: { from: 'now-30d', to: 'now' }, + selectedMetricField: 'transaction.duration.us', + time: { from: 'now-15m', to: 'now' }, }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx new file mode 100644 index 000000000000..fbd7c34303d9 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesConfig, SeriesUrl } from '../../types'; +import { ReportDefinitionField } from './report_definition_field'; + +export function ReportDefinitionCol({ + seriesId, + series, + seriesConfig, +}: { + seriesId: number; + series: SeriesUrl; + seriesConfig: SeriesConfig; +}) { + const { setSeries } = useSeriesStorage(); + + const { reportDefinitions: selectedReportDefinitions = {} } = series; + + const { definitionFields } = seriesConfig; + + const onChange = (field: string, value?: string[]) => { + if (!value?.[0]) { + delete selectedReportDefinitions[field]; + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions }, + }); + } else { + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions, [field]: value }, + }); + } + }; + + return ( + + {definitionFields.map((field) => ( + + + + ))} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx index 8a83b5c2a8cb..3651b4b7f075 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx @@ -6,30 +6,25 @@ */ import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash'; import { ExistsFilter } from '@kbn/es-query'; import FieldValueSuggestions from '../../../field_value_suggestions'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../../lens/common'; import { buildPhrasesFilter } from '../../configurations/utils'; -import { SeriesConfig } from '../../types'; +import { SeriesConfig, SeriesUrl } from '../../types'; import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_value_combobox'; interface Props { - seriesId: string; + seriesId: number; + series: SeriesUrl; field: string; seriesConfig: SeriesConfig; onChange: (field: string, value?: string[]) => void; } -export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange }: Props) { - const { getSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - +export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) { const { indexPattern } = useAppIndexPatternContext(series.dataType); const { reportDefinitions: selectedReportDefinitions = {} } = series; @@ -64,23 +59,26 @@ export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(selectedReportDefinitions), JSON.stringify(baseFilters)]); + if (!indexPattern) { + return null; + } + return ( - - - {indexPattern && ( - onChange(field, val)} - filters={queryFilters} - time={series.time} - fullWidth={true} - allowAllValuesSelection={true} - /> - )} - - + onChange(field, val)} + filters={queryFilters} + time={series.time} + fullWidth={true} + asCombobox={true} + allowExclusions={false} + allowAllValuesSelection={true} + usePrependLabel={false} + compressed={false} + required={isEmpty(selectedReportDefinitions)} + /> ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx new file mode 100644 index 000000000000..31a8c7cb7bfa --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSuperSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { ReportViewType } from '../../types'; +import { + CORE_WEB_VITALS_LABEL, + DEVICE_DISTRIBUTION_LABEL, + KPI_OVER_TIME_LABEL, + PERF_DIST_LABEL, +} from '../../configurations/constants/labels'; + +const SELECT_REPORT_TYPE = 'SELECT_REPORT_TYPE'; + +export const reportTypesList: Array<{ + reportType: ReportViewType | typeof SELECT_REPORT_TYPE; + label: string; +}> = [ + { + reportType: SELECT_REPORT_TYPE, + label: i18n.translate('xpack.observability.expView.reportType.selectLabel', { + defaultMessage: 'Select report type', + }), + }, + { reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, + { reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, +]; + +export function ReportTypesSelect() { + const { setReportType, reportType: selectedReportType, allSeries } = useSeriesStorage(); + + const onReportTypeChange = (reportType: ReportViewType) => { + setReportType(reportType); + }; + + const options = reportTypesList + .filter(({ reportType }) => (selectedReportType ? reportType !== SELECT_REPORT_TYPE : true)) + .map(({ reportType, label }) => ({ + value: reportType, + inputDisplay: reportType === SELECT_REPORT_TYPE ? label : {label}, + dropdownDisplay: label, + })); + + return ( + onReportTypeChange(value as ReportViewType)} + style={{ minWidth: 200 }} + isInvalid={!selectedReportType && allSeries.length > 0} + disabled={allSeries.length > 0} + /> + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx similarity index 59% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx index eb76772a66c7..64291f84f766 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx @@ -7,10 +7,10 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; import { SelectedFilters } from './selected_filters'; -import { getDefaultConfigs } from '../configurations/default_configs'; -import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('SelectedFilters', function () { mockAppIndexPattern(); @@ -22,11 +22,19 @@ describe('SelectedFilters', function () { }); it('should render properly', async function () { - const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; + const initSeries = { filters }; - render(, { - initSeries, - }); + render( + , + { + initSeries, + } + ); await waitFor(() => { screen.getByText('Chrome'); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx new file mode 100644 index 000000000000..3327ecf1fc9b --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment } from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FilterLabel } from '../../components/filter_label'; +import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { useSeriesFilters } from '../../hooks/use_series_filters'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: number; + series: SeriesUrl; + seriesConfig: SeriesConfig; +} +export function SelectedFilters({ seriesId, series, seriesConfig }: Props) { + const { setSeries } = useSeriesStorage(); + + const { labels } = seriesConfig; + + const filters: UrlFilter[] = series.filters ?? []; + + const { removeFilter } = useSeriesFilters({ seriesId, series }); + + const { indexPattern } = useAppIndexPatternContext(series.dataType); + + if (filters.length === 0 || !indexPattern) { + return null; + } + + return ( + <> + + {filters.map(({ field, values, notValues }) => ( + + {(values ?? []).length > 0 && ( + + { + values?.forEach((val) => { + removeFilter({ field, value: val, negate: false }); + }); + }} + negate={false} + indexPattern={indexPattern} + /> + + )} + {(notValues ?? []).length > 0 && ( + + { + values?.forEach((val) => { + removeFilter({ field, value: val, negate: false }); + }); + }} + indexPattern={indexPattern} + /> + + )} + + ))} + + {(series.filters ?? []).length > 0 && ( + + { + setSeries(seriesId, { ...series, filters: undefined }); + }} + size="xs" + > + {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { + defaultMessage: 'Clear filters', + })} + + + )} + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx index 51ebe6c6bd9d..37b5b1571f84 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -6,98 +6,113 @@ */ import React from 'react'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash'; import { RemoveSeries } from './remove_series'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesUrl } from '../../types'; +import { SeriesConfig, SeriesUrl } from '../../types'; +import { useDiscoverLink } from '../../hooks/use_discover_link'; interface Props { - seriesId: string; - editorMode?: boolean; + seriesId: number; + series: SeriesUrl; + seriesConfig?: SeriesConfig; + onEditClick?: () => void; } -export function SeriesActions({ seriesId, editorMode = false }: Props) { - const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage(); - const series = getSeries(seriesId); - const onEdit = () => { - setSeries(seriesId, { ...series, isNew: true }); - }; +export function SeriesActions({ seriesId, series, seriesConfig, onEditClick }: Props) { + const { setSeries, allSeries } = useSeriesStorage(); + + const { href: discoverHref } = useDiscoverLink({ series, seriesConfig }); const copySeries = () => { - let copySeriesId: string = `${seriesId}-copy`; - if (allSeriesIds.includes(copySeriesId)) { - copySeriesId = copySeriesId + allSeriesIds.length; + let copySeriesId: string = `${series.name}-copy`; + if (allSeries.find(({ name }) => name === copySeriesId)) { + copySeriesId = copySeriesId + allSeries.length; } - setSeries(copySeriesId, series); + setSeries(allSeries.length, { ...series, name: copySeriesId }); }; - const { reportType, reportDefinitions, isNew, ...restSeries } = series; - const isSaveAble = reportType && !isEmpty(reportDefinitions); - - const saveSeries = () => { - if (isSaveAble) { - const reportDefId = Object.values(reportDefinitions ?? {})[0]; - let newSeriesId = `${reportDefId}-${reportType}`; - - if (allSeriesIds.includes(newSeriesId)) { - newSeriesId = `${newSeriesId}-${allSeriesIds.length}`; - } - const newSeriesN: SeriesUrl = { - ...restSeries, - reportType, - reportDefinitions, - }; - - setSeries(newSeriesId, newSeriesN); - removeSeries(seriesId); + const toggleSeries = () => { + if (series.hidden) { + setSeries(seriesId, { ...series, hidden: undefined }); + } else { + setSeries(seriesId, { ...series, hidden: true }); } }; return ( - - {!editorMode && ( - + + + + + + + + - - )} - {editorMode && ( - + + + + + - - )} - {editorMode && ( - + + + + + - - )} + + ); } + +const EDIT_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.edit', { + defaultMessage: 'Edit series', +}); + +const HIDE_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.hide', { + defaultMessage: 'Hide series', +}); + +const COPY_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.clone', { + defaultMessage: 'Copy series', +}); + +const VIEW_SAMPLE_DOCUMENTS_LABEL = i18n.translate( + 'xpack.observability.seriesEditor.sampleDocuments', + { + defaultMessage: 'View sample documents in new tab', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx index 02144c6929b3..5b576d9da017 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -5,29 +5,17 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; -import React, { useState, Fragment } from 'react'; -import { - EuiButton, - EuiPopover, - EuiSpacer, - EuiButtonEmpty, - EuiFlexItem, - EuiFlexGroup, -} from '@elastic/eui'; +import React from 'react'; +import { EuiFilterGroup, EuiSpacer } from '@elastic/eui'; import { FilterExpanded } from './filter_expanded'; -import { SeriesConfig } from '../../types'; +import { SeriesConfig, SeriesUrl } from '../../types'; import { FieldLabels } from '../../configurations/constants/constants'; -import { SelectedFilters } from '../selected_filters'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SelectedFilters } from './selected_filters'; interface Props { - seriesId: string; - filterFields: SeriesConfig['filterFields']; - baseFilters: SeriesConfig['baseFilters']; + seriesId: number; seriesConfig: SeriesConfig; - isNew?: boolean; - labels?: Record; + series: SeriesUrl; } export interface Field { @@ -37,119 +25,38 @@ export interface Field { isNegated?: boolean; } -export function SeriesFilter({ - seriesConfig, - isNew, - seriesId, - filterFields = [], - baseFilters, - labels, -}: Props) { - const [isPopoverVisible, setIsPopoverVisible] = useState(false); - - const [selectedField, setSelectedField] = useState(); - - const options: Field[] = filterFields.map((field) => { +export function SeriesFilter({ series, seriesConfig, seriesId }: Props) { + const options: Field[] = seriesConfig.filterFields.map((field) => { if (typeof field === 'string') { - return { label: labels?.[field] ?? FieldLabels[field], field }; + return { label: seriesConfig.labels?.[field] ?? FieldLabels[field], field }; } return { field: field.field, nested: field.nested, isNegated: field.isNegated, - label: labels?.[field.field] ?? FieldLabels[field.field], + label: seriesConfig.labels?.[field.field] ?? FieldLabels[field.field], }; }); - const { setSeries, getSeries } = useSeriesStorage(); - const urlSeries = getSeries(seriesId); - - const button = ( - { - setIsPopoverVisible((prevState) => !prevState); - }} - size="s" - > - {i18n.translate('xpack.observability.expView.seriesEditor.addFilter', { - defaultMessage: 'Add filter', - })} - - ); - - const mainPanel = ( + return ( <> + + {options.map((opt) => ( + + ))} + - {options.map((opt) => ( - - { - setSelectedField(opt); - }} - > - {opt.label} - - - - ))} + ); - - const childPanel = selectedField ? ( - { - setSelectedField(undefined); - }} - filters={baseFilters} - /> - ) : null; - - const closePopover = () => { - setIsPopoverVisible(false); - setSelectedField(undefined); - }; - - return ( - - - - - {!selectedField ? mainPanel : childPanel} - - - {(urlSeries.filters ?? []).length > 0 && ( - - { - setSeries(seriesId, { ...urlSeries, filters: undefined }); - }} - size="s" - > - {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { - defaultMessage: 'Clear filters', - })} - - - )} - - ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_info.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_info.tsx new file mode 100644 index 000000000000..4c2e57e78055 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_info.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SeriesConfig, SeriesUrl } from '../../types'; +import { SeriesColorPicker } from '../../components/series_color_picker'; +import { SeriesChartTypes } from './chart_type_select'; + +interface Props { + seriesId: number; + series: SeriesUrl; + seriesConfig?: SeriesConfig; +} + +export function SeriesInfo({ seriesId, series, seriesConfig }: Props) { + if (!seriesConfig) { + return null; + } + + return ( + + + + + + + + + ); + + return null; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx new file mode 100644 index 000000000000..ccad46120931 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { mockUxSeries, render } from '../../rtl_helpers'; +import { SeriesName } from './series_name'; + +describe.skip('SeriesChartTypesSelect', function () { + it('should render properly', async function () { + render(); + + expect(screen.getByText(mockUxSeries.name)).toBeInTheDocument(); + }); + + it('should display input when editing name', async function () { + render(); + + let input = screen.queryByLabelText(mockUxSeries.name); + + // read only + expect(input).not.toBeInTheDocument(); + + const editButton = screen.getByRole('button'); + // toggle editing + fireEvent.click(editButton); + + await waitFor(() => { + input = screen.getByLabelText(mockUxSeries.name); + + expect(input).toBeInTheDocument(); + }); + + // toggle readonly + fireEvent.click(editButton); + + await waitFor(() => { + input = screen.getByLabelText(mockUxSeries.name); + + expect(input).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx new file mode 100644 index 000000000000..cff30a2b3505 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, ChangeEvent, useEffect, useRef } from 'react'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { + EuiFieldText, + EuiText, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiOutsideClickDetector, +} from '@elastic/eui'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesUrl } from '../../types'; + +interface Props { + seriesId: number; + series: SeriesUrl; +} + +export const StyledText = styled(EuiText)` + &.euiText.euiText--constrainedWidth { + max-width: 200px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } +`; + +export function SeriesName({ series, seriesId }: Props) { + const { setSeries } = useSeriesStorage(); + + const [value, setValue] = useState(series.name); + const [isEditingEnabled, setIsEditingEnabled] = useState(false); + const inputRef = useRef(null); + const buttonRef = useRef(null); + + const onChange = (e: ChangeEvent) => { + setValue(e.target.value); + }; + + const onSave = () => { + if (value !== series.name) { + setSeries(seriesId, { ...series, name: value }); + } + }; + + const onOutsideClick = (event: Event) => { + if (event.target !== buttonRef.current) { + setIsEditingEnabled(false); + } + }; + + useEffect(() => { + setValue(series.name); + }, [series.name]); + + useEffect(() => { + if (isEditingEnabled && inputRef.current) { + inputRef.current.focus(); + } + }, [isEditingEnabled, inputRef]); + + return ( + + {isEditingEnabled ? ( + + + + + + ) : ( + + {value} + + )} + + setIsEditingEnabled(!isEditingEnabled)} + iconType="pencil" + aria-label={i18n.translate('xpack.observability.expView.seriesEditor.editName', { + defaultMessage: 'Edit name', + })} + color="text" + buttonRef={buttonRef} + /> + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx new file mode 100644 index 000000000000..9f4de1b6dd51 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiHorizontalRule } from '@elastic/eui'; +import { SeriesConfig, SeriesUrl } from '../types'; +import { ReportDefinitionCol } from './columns/report_definition_col'; +import { OperationTypeSelect } from './columns/operation_type_select'; +import { parseCustomFieldName } from '../configurations/lens_attributes'; +import { SeriesFilter } from './columns/series_filter'; +import { DatePickerCol } from './columns/date_picker_col'; +import { Breakdowns } from './columns/breakdowns'; + +function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { + const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); + + return columnType; +} + +interface Props { + seriesId: number; + series: SeriesUrl; + seriesConfig?: SeriesConfig; +} +export function ExpandedSeriesRow(seriesProps: Props) { + const { seriesConfig, series, seriesId } = seriesProps; + + if (!seriesConfig) { + return null; + } + + const { selectedMetricField } = series ?? {}; + + const { hasOperationType, yAxisColumns } = seriesConfig; + + const columnType = getColumnType(seriesConfig, selectedMetricField); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + {(hasOperationType || columnType === 'operation') && ( + + + + + + )} + +
+ ); +} + +const BREAKDOWNS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.breakdowns', { + defaultMessage: 'Breakdowns', +}); + +const FILTERS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.selectFilters', { + defaultMessage: 'Filters', +}); + +const OPERATION_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.operation', { + defaultMessage: 'Operation', +}); + +const DATE_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.date', { + defaultMessage: 'Date', +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx new file mode 100644 index 000000000000..496e7a10f9c4 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { + EuiToolTip, + EuiPopover, + EuiButton, + EuiListGroup, + EuiListGroupItem, + EuiBadge, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { SeriesConfig, SeriesUrl } from '../types'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; + +interface Props { + seriesId: number; + series: SeriesUrl; + defaultValue?: string; + seriesConfig?: SeriesConfig; +} + +export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { + const { setSeries } = useSeriesStorage(); + const [showOptions, setShowOptions] = useState(false); + const metricOptions = seriesConfig?.metricOptions; + + const { indexPatterns } = useAppIndexPatternContext(); + + const onChange = (value?: string) => { + setSeries(seriesId, { + ...series, + selectedMetricField: value, + }); + }; + + if (!series.dataType) { + return null; + } + + const indexPattern = indexPatterns?.[series.dataType]; + + const options = (metricOptions ?? []).map(({ label, field, id }) => { + let disabled = false; + + if (field !== RECORDS_FIELD && field !== RECORDS_PERCENTAGE_FIELD && field) { + disabled = !Boolean(indexPattern?.getFieldByName(field)); + } + return { + disabled, + value: field || id, + dropdownDisplay: disabled ? ( + {field}, + }} + /> + } + > + {label} + + ) : ( + label + ), + inputDisplay: label, + }; + }); + + return ( + <> + {!series.selectedMetricField && ( + setShowOptions((prevState) => !prevState)} + fill + size="s" + > + {SELECT_REPORT_METRIC_LABEL} + + } + isOpen={showOptions} + closePopover={() => setShowOptions((prevState) => !prevState)} + > + + {options.map((option) => ( + onChange(option.value)} + label={option.dropdownDisplay} + isDisabled={option.disabled} + /> + ))} + + + )} + {series.selectedMetricField && ( + onChange(undefined)} + iconOnClickAriaLabel={REMOVE_REPORT_METRIC_LABEL} + > + { + seriesConfig?.metricOptions?.find((option) => option.id === series.selectedMetricField) + ?.label + } + + )} + + ); +} + +const SELECT_REPORT_METRIC_LABEL = i18n.translate( + 'xpack.observability.expView.seriesEditor.selectReportMetric', + { + defaultMessage: 'Select report metric', + } +); + +const REMOVE_REPORT_METRIC_LABEL = i18n.translate( + 'xpack.observability.expView.seriesEditor.removeReportMetric', + { + defaultMessage: 'Remove report metric', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx deleted file mode 100644 index 5d2ce6ba8495..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { FilterLabel } from '../components/filter_label'; -import { SeriesConfig, UrlFilter } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { useSeriesFilters } from '../hooks/use_series_filters'; -import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; - -interface Props { - seriesId: string; - seriesConfig: SeriesConfig; - isNew?: boolean; -} -export function SelectedFilters({ seriesId, isNew, seriesConfig }: Props) { - const { getSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - - const { reportDefinitions = {} } = series; - - const { labels } = seriesConfig; - - const filters: UrlFilter[] = series.filters ?? []; - - let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); - - // we don't want to display report definition filters in new series view - if (isNew) { - definitionFilters = []; - } - - const { removeFilter } = useSeriesFilters({ seriesId }); - - const { indexPattern } = useAppIndexPatternContext(series.dataType); - - return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? ( - - - {filters.map(({ field, values, notValues }) => ( - - {(values ?? []).map((val) => ( - - removeFilter({ field, value: val, negate: false })} - negate={false} - indexPattern={indexPattern} - /> - - ))} - {(notValues ?? []).map((val) => ( - - removeFilter({ field, value: val, negate: true })} - indexPattern={indexPattern} - /> - - ))} - - ))} - - {definitionFilters.map(({ field, values }) => ( - - {(values ?? []).map((val) => ( - - { - // FIXME handle this use case - }} - negate={false} - definitionFilter={true} - indexPattern={indexPattern} - /> - - ))} - - ))} - - - ) : null; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx new file mode 100644 index 000000000000..ea47ccd0b042 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { EuiFlexItem, EuiFlexGroup, EuiPanel, EuiAccordion, EuiSpacer } from '@elastic/eui'; +import { BuilderItem } from '../types'; +import { SeriesActions } from './columns/series_actions'; +import { SeriesInfo } from './columns/series_info'; +import { DataTypesSelect } from './columns/data_type_select'; +import { IncompleteBadge } from './columns/incomplete_badge'; +import { ExpandedSeriesRow } from './expanded_series_row'; +import { SeriesName } from './columns/series_name'; +import { ReportMetricOptions } from './report_metric_options'; + +const StyledAccordion = styled(EuiAccordion)` + .euiAccordion__button { + width: auto; + flex-grow: 0; + } + + .euiAccordion__optionalAction { + flex-grow: 1; + flex-shrink: 1; + } +`; + +interface Props { + item: BuilderItem; + isExpanded: boolean; + toggleExpanded: () => void; +} + +export function Series({ item, isExpanded, toggleExpanded }: Props) { + const { id } = item; + const seriesProps = { + ...item, + seriesId: id, + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + > + + + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index c3cc8484d175..d13857b5e966 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -5,134 +5,226 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { SeriesFilter } from './columns/series_filter'; -import { SeriesConfig } from '../types'; -import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; +import { + EuiSpacer, + EuiFormRow, + EuiFlexItem, + EuiFlexGroup, + EuiButtonEmpty, + EuiHorizontalRule, +} from '@elastic/eui'; +import { rgba } from 'polished'; +import { euiStyled } from './../../../../../../../../src/plugins/kibana_react/common'; +import { AppDataType, ReportViewType, BuilderItem } from '../types'; +import { SeriesContextValue, useSeriesStorage } from '../hooks/use_series_storage'; +import { IndexPatternState, useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { DatePickerCol } from './columns/date_picker_col'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { SeriesActions } from './columns/series_actions'; -import { ChartEditOptions } from './chart_edit_options'; +import { ReportTypesSelect } from './columns/report_type_select'; +import { ViewActions } from '../views/view_actions'; +import { Series } from './series'; -interface EditItem { - seriesConfig: SeriesConfig; +export interface ReportTypeItem { id: string; + reportType: ReportViewType; + label: string; } -export function SeriesEditor() { - const { allSeries, allSeriesIds } = useSeriesStorage(); - - const columns = [ - { - name: i18n.translate('xpack.observability.expView.seriesEditor.name', { - defaultMessage: 'Name', - }), - field: 'id', - width: '15%', - render: (seriesId: string) => ( - - {' '} - {seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId} - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { - defaultMessage: 'Filters', - }), - field: 'defaultFilters', - width: '15%', - render: (seriesId: string, { seriesConfig, id }: EditItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', { - defaultMessage: 'Breakdowns', - }), - field: 'id', - width: '25%', - render: (seriesId: string, { seriesConfig, id }: EditItem) => ( - - ), - }, - { - name: ( -
- -
- ), - width: '20%', - field: 'id', - align: 'right' as const, - render: (seriesId: string, item: EditItem) => , - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.actions', { - defaultMessage: 'Actions', - }), - align: 'center' as const, - width: '10%', - field: 'id', - render: (seriesId: string, item: EditItem) => , - }, - ]; - - const { indexPatterns } = useAppIndexPatternContext(); - const items: EditItem[] = []; - - allSeriesIds.forEach((seriesKey) => { - const series = allSeries[seriesKey]; - if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) { - items.push({ - id: seriesKey, - seriesConfig: getDefaultConfigs({ - indexPattern: indexPatterns[series.dataType], - reportType: series.reportType, - dataType: series.dataType, - }), +type ExpandedRowMap = Record; + +export const getSeriesToEdit = ({ + indexPatterns, + allSeries, + reportType, +}: { + allSeries: SeriesContextValue['allSeries']; + indexPatterns: IndexPatternState; + reportType: ReportViewType; +}): BuilderItem[] => { + const getDataViewSeries = (dataType: AppDataType) => { + if (indexPatterns?.[dataType]) { + return getDefaultConfigs({ + dataType, + reportType, + indexPattern: indexPatterns[dataType], }); } + }; + + return allSeries.map((series, seriesIndex) => { + const seriesConfig = getDataViewSeries(series.dataType)!; + + return { id: seriesIndex, series, seriesConfig }; }); +}; - if (items.length === 0 && allSeriesIds.length > 0) { - return null; - } +export const SeriesEditor = React.memo(function () { + const [editorItems, setEditorItems] = useState([]); + + const { getSeries, allSeries, reportType, removeSeries } = useSeriesStorage(); + + const { loading, indexPatterns } = useAppIndexPatternContext(); + + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>({}); + + const [{ prevCount, curCount }, setSeriesCount] = useState<{ + prevCount?: number; + curCount: number; + }>({ + curCount: allSeries.length, + }); + + useEffect(() => { + setSeriesCount((oldParams) => ({ prevCount: oldParams.curCount, curCount: allSeries.length })); + if (typeof prevCount !== 'undefined' && !isNaN(prevCount) && prevCount < curCount) { + setItemIdToExpandedRowMap({}); + } + }, [allSeries.length, curCount, prevCount]); + + useEffect(() => { + const newExpandRows: ExpandedRowMap = {}; + + setEditorItems((prevState) => { + const newEditorItems = getSeriesToEdit({ + reportType, + allSeries, + indexPatterns, + }); + + newEditorItems.forEach(({ series, id }) => { + const prevSeriesItem = prevState.find(({ id: prevId }) => prevId === id); + if ( + prevSeriesItem && + series.selectedMetricField && + prevSeriesItem.series.selectedMetricField !== series.selectedMetricField + ) { + newExpandRows[id] = true; + } + }); + return [...newEditorItems]; + }); + + setItemIdToExpandedRowMap((prevState) => { + return { ...prevState, ...newExpandRows }; + }); + }, [allSeries, getSeries, indexPatterns, loading, reportType]); + + const toggleDetails = (item: BuilderItem) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + if (itemIdToExpandedRowMapValues[item.id]) { + delete itemIdToExpandedRowMapValues[item.id]; + } else { + itemIdToExpandedRowMapValues[item.id] = true; + } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }; + + const resetView = () => { + const totalSeries = allSeries.length; + for (let i = totalSeries; i >= 0; i--) { + removeSeries(i); + } + setEditorItems([]); + setItemIdToExpandedRowMap({}); + }; return ( - <> - - - - + +
+ + + + + + + {reportType && ( + + resetView()} color="text"> + {RESET_LABEL} + + + )} + + + + + + + {editorItems.map((item) => ( +
+ toggleDetails(item)} + isExpanded={itemIdToExpandedRowMap[item.id]} + /> + +
+ ))} + +
+
); -} +}); + +const Wrapper = euiStyled.div` + &::-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; + } + + &&& { + .euiTableRow-isExpandedRow .euiTableRowCell { + border-top: none; + background-color: #FFFFFF; + border-bottom: 2px solid #d3dae6; + border-right: 2px solid rgb(211, 218, 230); + border-left: 2px solid rgb(211, 218, 230); + } + + .isExpanded { + border-right: 2px solid rgb(211, 218, 230); + border-left: 2px solid rgb(211, 218, 230); + .euiTableRowCell { + border-bottom: none; + } + } + .isIncomplete .euiTableRowCell { + background-color: rgba(254, 197, 20, 0.1); + } + } +`; + +export const LOADING_VIEW = i18n.translate( + 'xpack.observability.expView.seriesBuilder.loadingView', + { + defaultMessage: 'Loading view ...', + } +); + +export const SELECT_REPORT_TYPE = i18n.translate( + 'xpack.observability.expView.seriesBuilder.selectReportType', + { + defaultMessage: 'No report type selected', + } +); + +export const RESET_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.reset', { + defaultMessage: 'Reset', +}); + +export const REPORT_TYPE_LABEL = i18n.translate( + 'xpack.observability.expView.seriesBuilder.reportType', + { + defaultMessage: 'Report type', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 9817899412ce..f3592a749a2c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -6,7 +6,7 @@ */ import { PaletteOutput } from 'src/plugins/charts/public'; -import { ExistsFilter } from '@kbn/es-query'; +import { ExistsFilter, PhraseFilter } from '@kbn/es-query'; import { LastValueIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -42,7 +42,7 @@ export interface MetricOption { field?: string; label: string; description?: string; - columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN'; + columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN' | 'unique_count'; columnFilters?: ColumnFilter[]; timeScale?: string; } @@ -55,7 +55,7 @@ export interface SeriesConfig { defaultSeriesType: SeriesType; filterFields: Array; seriesTypes: SeriesType[]; - baseFilters?: PersistableFilter[] | ExistsFilter[]; + baseFilters?: Array; definitionFields: string[]; metricOptions?: MetricOption[]; labels: Record; @@ -69,6 +69,7 @@ export interface SeriesConfig { export type URLReportDefinition = Record; export interface SeriesUrl { + name: string; time: { to: string; from: string; @@ -76,12 +77,12 @@ export interface SeriesUrl { breakdown?: string; filters?: UrlFilter[]; seriesType?: SeriesType; - reportType: ReportViewType; operationType?: OperationType; dataType: AppDataType; reportDefinitions?: URLReportDefinition; selectedMetricField?: string; - isNew?: boolean; + hidden?: boolean; + color?: string; } export interface UrlFilter { @@ -116,3 +117,9 @@ export interface FieldFormat { params: FieldFormatParams; }; } + +export interface BuilderItem { + id: number; + series: SeriesUrl; + seriesConfig?: SeriesConfig; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.test.tsx new file mode 100644 index 000000000000..978296a295ef --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, waitFor, fireEvent } from '@testing-library/dom'; +import { render } from '../rtl_helpers'; +import { AddSeriesButton } from './add_series_button'; +import { DEFAULT_TIME, ReportTypes } from '../configurations/constants'; +import * as hooks from '../hooks/use_series_storage'; + +const setSeries = jest.fn(); + +describe('AddSeriesButton', () => { + beforeEach(() => { + jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({ + ...jest.requireActual('../hooks/use_series_storage'), + allSeries: [], + setSeries, + reportType: ReportTypes.KPI, + }); + setSeries.mockClear(); + }); + + it('renders AddSeriesButton', async () => { + render(); + + expect(screen.getByText(/Add series/i)).toBeInTheDocument(); + }); + + it('calls setSeries when AddSeries Button is clicked', async () => { + const { rerender } = render(); + let addSeriesButton = screen.getByText(/Add series/i); + + fireEvent.click(addSeriesButton); + + await waitFor(() => { + expect(setSeries).toBeCalledTimes(1); + expect(setSeries).toBeCalledWith(0, { name: 'new-series-1', time: DEFAULT_TIME }); + }); + + jest.clearAllMocks(); + jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({ + ...jest.requireActual('../hooks/use_series_storage'), + allSeries: new Array(1), + setSeries, + reportType: ReportTypes.KPI, + }); + + rerender(); + + addSeriesButton = screen.getByText(/Add series/i); + + fireEvent.click(addSeriesButton); + + await waitFor(() => { + expect(setSeries).toBeCalledTimes(1); + expect(setSeries).toBeCalledWith(1, { name: 'new-series-2', time: DEFAULT_TIME }); + }); + }); + + it.each([ReportTypes.DEVICE_DISTRIBUTION, ReportTypes.CORE_WEB_VITAL])( + 'does not allow adding more than 1 series for core web vitals or device distribution', + async (reportType) => { + jest.clearAllMocks(); + jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({ + ...jest.requireActual('../hooks/use_series_storage'), + allSeries: new Array(1), // mock array of length 1 + setSeries, + reportType, + }); + + render(); + const addSeriesButton = screen.getByText(/Add series/i); + expect(addSeriesButton.closest('button')).toBeDisabled(); + + fireEvent.click(addSeriesButton); + + await waitFor(() => { + expect(setSeries).toBeCalledTimes(0); + }); + } + ); + + it('does not allow adding a series when the report type is undefined', async () => { + jest.clearAllMocks(); + jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({ + ...jest.requireActual('../hooks/use_series_storage'), + allSeries: [], + setSeries, + }); + + render(); + const addSeriesButton = screen.getByText(/Add series/i); + expect(addSeriesButton.closest('button')).toBeDisabled(); + + fireEvent.click(addSeriesButton); + + await waitFor(() => { + expect(setSeries).toBeCalledTimes(0); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx new file mode 100644 index 000000000000..71b16c9c0e68 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; + +import { EuiToolTip, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { SeriesUrl, BuilderItem } from '../types'; +import { getSeriesToEdit } from '../series_editor/series_editor'; +import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { DEFAULT_TIME, ReportTypes } from '../configurations/constants'; + +export function AddSeriesButton() { + const [editorItems, setEditorItems] = useState([]); + const { getSeries, allSeries, setSeries, reportType } = useSeriesStorage(); + + const { loading, indexPatterns } = useAppIndexPatternContext(); + + useEffect(() => { + setEditorItems(getSeriesToEdit({ allSeries, indexPatterns, reportType })); + }, [allSeries, getSeries, indexPatterns, loading, reportType]); + + const addSeries = () => { + const prevSeries = allSeries?.[0]; + const name = `${NEW_SERIES_KEY}-${editorItems.length + 1}`; + const nextSeries = { name } as SeriesUrl; + + const nextSeriesId = allSeries.length; + + if (reportType === 'data-distribution') { + setSeries(nextSeriesId, { + ...nextSeries, + time: prevSeries?.time || DEFAULT_TIME, + } as SeriesUrl); + } else { + setSeries( + nextSeriesId, + prevSeries ? nextSeries : ({ ...nextSeries, time: DEFAULT_TIME } as SeriesUrl) + ); + } + }; + + const isAddDisabled = + !reportType || + ((reportType === ReportTypes.CORE_WEB_VITAL || + reportType === ReportTypes.DEVICE_DISTRIBUTION) && + allSeries.length > 0); + + return ( + + addSeries()} + isDisabled={isAddDisabled} + iconType="plusInCircle" + size="s" + > + {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { + defaultMessage: 'Add series', + })} + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx new file mode 100644 index 000000000000..00fbc8c0e522 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { RefObject } from 'react'; + +import { SeriesEditor } from '../series_editor/series_editor'; +import { AddSeriesButton } from './add_series_button'; +import { PanelId } from '../exploratory_view'; + +export function SeriesViews({ + seriesBuilderRef, +}: { + seriesBuilderRef: RefObject; + onSeriesPanelCollapse: (panel: PanelId) => void; +}) { + return ( +
+ + +
+ ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx new file mode 100644 index 000000000000..f4416ef60441 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEqual } from 'lodash'; +import { allSeriesKey, convertAllShortSeries, useSeriesStorage } from '../hooks/use_series_storage'; + +export function ViewActions() { + const { allSeries, storage, applyChanges } = useSeriesStorage(); + + const noChanges = isEqual(allSeries, convertAllShortSeries(storage.get(allSeriesKey) ?? [])); + + return ( + + + applyChanges()} isDisabled={noChanges} fill size="s"> + {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { + defaultMessage: 'Apply changes', + })} + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx index fc562fa80e26..0735df53888a 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx @@ -6,15 +6,24 @@ */ import React, { useEffect, useState } from 'react'; -import { union } from 'lodash'; -import { EuiComboBox, EuiFormControlLayout, EuiComboBoxOptionOption } from '@elastic/eui'; +import { union, isEmpty } from 'lodash'; +import { + EuiComboBox, + EuiFormControlLayout, + EuiComboBoxOptionOption, + EuiFormRow, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { FieldValueSelectionProps } from './types'; export const ALL_VALUES_SELECTED = 'ALL_VALUES'; const formatOptions = (values?: string[], allowAllValuesSelection?: boolean) => { const uniqueValues = Array.from( - new Set(allowAllValuesSelection ? ['ALL_VALUES', ...(values ?? [])] : values) + new Set( + allowAllValuesSelection && (values ?? []).length > 0 + ? ['ALL_VALUES', ...(values ?? [])] + : values + ) ); return (uniqueValues ?? []).map((label) => ({ @@ -30,7 +39,9 @@ export function FieldValueCombobox({ loading, values, setQuery, + usePrependLabel = true, compressed = true, + required = true, allowAllValuesSelection, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -54,29 +65,35 @@ export function FieldValueCombobox({ onSelectionChange(selectedValuesN.map(({ label: lbl }) => lbl)); }; - return ( + const comboBox = ( + { + setQuery(searchVal); + }} + options={options} + selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} + onChange={onChange} + isInvalid={required && isEmpty(selectedValue)} + /> + ); + + return usePrependLabel ? ( - { - setQuery(searchVal); - }} - options={options} - selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} - onChange={onChange} - /> + {comboBox} + ) : ( + + {comboBox} + ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx index aca29c472368..dfcd917cf534 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx @@ -70,8 +70,8 @@ export function FieldValueSelection({ values = [], selectedValue, excludedValue, - compressed = true, allowExclusions = true, + compressed = true, onChange: onSelectionChange, }: FieldValueSelectionProps) { const [options, setOptions] = useState(() => @@ -174,8 +174,8 @@ export function FieldValueSelection({ }} options={options} onChange={onChange} - isLoading={loading && !query && options.length === 0} allowExclusions={allowExclusions} + isLoading={loading && !query && options.length === 0} > {(list, search) => (
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx index 556a8e705234..6671c43dd8c7 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx @@ -95,6 +95,7 @@ describe('FieldValueSuggestions', () => { selectedValue={[]} filters={[]} asCombobox={false} + allowExclusions={true} /> ); @@ -119,6 +120,7 @@ describe('FieldValueSuggestions', () => { excludedValue={['Pak']} filters={[]} asCombobox={false} + allowExclusions={true} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx index 3de158ba0622..1c5da15dd33d 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx @@ -28,9 +28,11 @@ export function FieldValueSuggestions({ singleSelection, compressed, asFilterButton, + usePrependLabel, allowAllValuesSelection, + required, + allowExclusions = true, cardinalityField, - allowExclusions, asCombobox = true, onChange: onSelectionChange, }: FieldValueSuggestionsProps) { @@ -67,8 +69,10 @@ export function FieldValueSuggestions({ width={width} compressed={compressed} asFilterButton={asFilterButton} - allowAllValuesSelection={allowAllValuesSelection} + usePrependLabel={usePrependLabel} allowExclusions={allowExclusions} + allowAllValuesSelection={allowAllValuesSelection} + required={required} /> ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index 046f98748cdf..b6de2bafdd85 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -23,10 +23,11 @@ interface CommonProps { compressed?: boolean; asFilterButton?: boolean; showCount?: boolean; + usePrependLabel?: boolean; + allowExclusions?: boolean; allowAllValuesSelection?: boolean; cardinalityField?: string; required?: boolean; - allowExclusions?: boolean; } export type FieldValueSuggestionsProps = CommonProps & { diff --git a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx index 01d727071770..9e7b96b02206 100644 --- a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx @@ -18,21 +18,25 @@ export function buildFilterLabel({ negate, }: { label: string; - value: string; + value: string | string[]; negate: boolean; field: string; indexPattern: IndexPattern; }) { const indexField = indexPattern.getFieldByName(field)!; - const filter = esFilters.buildPhraseFilter(indexField, value, indexPattern); + const filter = + value instanceof Array && value.length > 1 + ? esFilters.buildPhrasesFilter(indexField, value, indexPattern) + : esFilters.buildPhraseFilter(indexField, value as string, indexPattern); - filter.meta.value = value; + filter.meta.type = value instanceof Array && value.length > 1 ? 'phrases' : 'phrase'; + + filter.meta.value = value as string; filter.meta.key = label; filter.meta.alias = null; filter.meta.negate = negate; filter.meta.disabled = false; - filter.meta.type = 'phrase'; return filter; } @@ -40,10 +44,10 @@ export function buildFilterLabel({ interface Props { field: string; label: string; - value: string; + value: string | string[]; negate: boolean; - removeFilter: (field: string, value: string, notVal: boolean) => void; - invertFilter: (val: { field: string; value: string; negate: boolean }) => void; + removeFilter: (field: string, value: string | string[], notVal: boolean) => void; + invertFilter: (val: { field: string; value: string | string[]; negate: boolean }) => void; indexPattern: IndexPattern; allowExclusion?: boolean; } diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx index 9d557a40b798..afc053604fcd 100644 --- a/x-pack/plugins/observability/public/components/shared/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/index.tsx @@ -6,6 +6,7 @@ */ import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import type { CoreVitalProps, HeaderMenuPortalProps } from './types'; import type { FieldValueSuggestionsProps } from './field_value_suggestions/types'; @@ -26,7 +27,7 @@ const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); export function HeaderMenuPortal(props: HeaderMenuPortalProps) { return ( - + }> ); diff --git a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx index 82a0fc39b851..198b4092b0ed 100644 --- a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx +++ b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx @@ -7,7 +7,7 @@ import { useUiSetting } from '../../../../../src/plugins/kibana_react/public'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; -import { TimePickerQuickRange } from '../components/shared/exploratory_view/series_date_picker'; +import { TimePickerQuickRange } from '../components/shared/exploratory_view/components/series_date_picker'; export function useQuickTimeRanges() { const timePickerQuickRanges = useUiSetting( diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index 3b588c59260d..ace01aa851ce 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -36,6 +36,7 @@ import { EuiFlexItem, EuiContextMenuPanel, EuiPopover, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; @@ -248,40 +249,63 @@ function ObservabilityActions({ ]; }, [afterCaseSelection, casePermissions, timelines, event, statusActionItems, alertPermissions]); + const viewDetailsTextLabel = i18n.translate( + 'xpack.observability.alertsTable.viewDetailsTextLabel', + { + defaultMessage: 'View details', + } + ); + const viewInAppTextLabel = i18n.translate('xpack.observability.alertsTable.viewInAppTextLabel', { + defaultMessage: 'View in app', + }); + const moreActionsTextLabel = i18n.translate( + 'xpack.observability.alertsTable.moreActionsTextLabel', + { + defaultMessage: 'More actions', + } + ); + return ( <> - setFlyoutAlert(alert)} - data-test-subj="openFlyoutButton" - /> + + setFlyoutAlert(alert)} + data-test-subj="openFlyoutButton" + aria-label={viewDetailsTextLabel} + /> + - + + + {actionsMenuItems.length > 0 && ( toggleActionsPopover(eventId)} - data-test-subj="alerts-table-row-action-more" - /> + + toggleActionsPopover(eventId)} + data-test-subj="alerts-table-row-action-more" + /> + } isOpen={openActionsPopoverId === eventId} closePopover={closeActionsPopover} diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 118f0783f968..10843bbd1d5b 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -24,6 +24,7 @@ import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../src/plugins/data/public'; +import type { DiscoverStart } from '../../../../src/plugins/discover/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import type { HomePublicPluginSetup, @@ -58,6 +59,7 @@ export interface ObservabilityPublicPluginsStart { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; lens: LensPublicStart; + discover: DiscoverStart; } export type ObservabilityPublicStart = ReturnType; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 00e487da7f9b..ff03379e3996 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -99,7 +99,7 @@ export const routes = { }), }, }, - '/exploratory-view': { + '/exploratory-view/': { handler: () => { return ; }, @@ -112,18 +112,4 @@ export const routes = { }), }, }, - // enable this to test multi series architecture - // '/exploratory-view/multi': { - // handler: () => { - // return ; - // }, - // params: { - // query: t.partial({ - // rangeFrom: t.string, - // rangeTo: t.string, - // refreshPaused: jsonRt.pipe(t.boolean), - // refreshInterval: jsonRt.pipe(t.number), - // }), - // }, - // }, }; diff --git a/x-pack/plugins/reporting/server/lib/content_stream.ts b/x-pack/plugins/reporting/server/lib/content_stream.ts index 23cc8a302dbe..3c0fdaa91f32 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.ts @@ -179,28 +179,27 @@ export class ContentStream extends Duplex { return this.jobSize != null && this.bytesRead >= this.jobSize; } - async _read() { - try { - const content = this.chunksRead ? await this.readChunk() : await this.readHead(); - if (!content) { - this.logger.debug(`Chunk is empty.`); - this.push(null); - return; - } - - const buffer = this.decode(content); - - this.push(buffer); - this.chunksRead++; - this.bytesRead += buffer.byteLength; - - if (this.isRead()) { - this.logger.debug(`Read ${this.bytesRead} of ${this.jobSize} bytes.`); - this.push(null); - } - } catch (error) { - this.destroy(error); - } + _read() { + (this.chunksRead ? this.readChunk() : this.readHead()) + .then((content) => { + if (!content) { + this.logger.debug(`Chunk is empty.`); + this.push(null); + return; + } + + const buffer = this.decode(content); + + this.push(buffer); + this.chunksRead++; + this.bytesRead += buffer.byteLength; + + if (this.isRead()) { + this.logger.debug(`Read ${this.bytesRead} of ${this.jobSize} bytes.`); + this.push(null); + } + }) + .catch((err) => this.destroy(err)); } private async removeChunks() { diff --git a/x-pack/plugins/security/common/types.ts b/x-pack/plugins/security/common/types.ts index 690aced63b18..e6354841cc9e 100644 --- a/x-pack/plugins/security/common/types.ts +++ b/x-pack/plugins/security/common/types.ts @@ -12,3 +12,10 @@ export interface SessionInfo { canBeExtended: boolean; provider: AuthenticationProvider; } + +export enum LogoutReason { + 'SESSION_EXPIRED' = 'SESSION_EXPIRED', + 'AUTHENTICATION_ERROR' = 'AUTHENTICATION_ERROR', + 'LOGGED_OUT' = 'LOGGED_OUT', + 'UNAUTHENTICATED' = 'UNAUTHENTICATED', +} diff --git a/x-pack/plugins/security/public/authentication/login/components/index.ts b/x-pack/plugins/security/public/authentication/login/components/index.ts index 63928e4e82e3..a2c12a3c0055 100644 --- a/x-pack/plugins/security/public/authentication/login/components/index.ts +++ b/x-pack/plugins/security/public/authentication/login/components/index.ts @@ -5,5 +5,6 @@ * 2.0. */ +export type { LoginFormProps } from './login_form'; export { LoginForm, LoginFormMessageType } from './login_form'; export { DisabledLoginForm } from './disabled_login_form'; diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/index.ts b/x-pack/plugins/security/public/authentication/login/components/login_form/index.ts index d12ea30c784c..f1b469f669c0 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/index.ts +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/index.ts @@ -5,4 +5,5 @@ * 2.0. */ +export type { LoginFormProps } from './login_form'; export { LoginForm, MessageType as LoginFormMessageType } from './login_form'; diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx index f60e525e0949..b8808415fc60 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx @@ -36,7 +36,7 @@ import type { HttpStart, IHttpFetchError, NotificationsStart } from 'src/core/pu import type { LoginSelector, LoginSelectorProvider } from '../../../../../common/login_state'; import { LoginValidator } from './validate_login'; -interface Props { +export interface LoginFormProps { http: HttpStart; notifications: NotificationsStart; selector: LoginSelector; @@ -78,7 +78,7 @@ export enum PageMode { LoginHelp, } -export class LoginForm extends Component { +export class LoginForm extends Component { private readonly validator: LoginValidator; /** @@ -88,7 +88,7 @@ export class LoginForm extends Component { */ private readonly suggestedProvider?: LoginSelectorProvider; - constructor(props: Props) { + constructor(props: LoginFormProps) { super(props); this.validator = new LoginValidator({ shouldValidate: false }); @@ -513,7 +513,7 @@ export class LoginForm extends Component { ); window.location.href = location; - } catch (err) { + } catch (err: any) { this.props.notifications.toasts.addError( err?.body?.message ? new Error(err?.body?.message) : err, { diff --git a/x-pack/plugins/security/public/authentication/login/login_page.tsx b/x-pack/plugins/security/public/authentication/login/login_page.tsx index 40438ac1c78f..e22c38b956e8 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.tsx @@ -12,7 +12,6 @@ import classNames from 'classnames'; import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { BehaviorSubject } from 'rxjs'; -import { parse } from 'url'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -23,6 +22,8 @@ import { LOGOUT_REASON_QUERY_STRING_PARAMETER, } from '../../../common/constants'; import type { LoginState } from '../../../common/login_state'; +import type { LogoutReason } from '../../../common/types'; +import type { LoginFormProps } from './components'; import { DisabledLoginForm, LoginForm, LoginFormMessageType } from './components'; interface Props { @@ -36,36 +37,33 @@ interface State { loginState: LoginState | null; } -const messageMap = new Map([ - [ - 'SESSION_EXPIRED', - { - type: LoginFormMessageType.Info, - content: i18n.translate('xpack.security.login.sessionExpiredDescription', { - defaultMessage: 'Your session has timed out. Please log in again.', - }), - }, - ], - [ - 'LOGGED_OUT', - { - type: LoginFormMessageType.Info, - content: i18n.translate('xpack.security.login.loggedOutDescription', { - defaultMessage: 'You have logged out of Elastic.', - }), - }, - ], - [ - 'UNAUTHENTICATED', - { - type: LoginFormMessageType.Danger, - content: i18n.translate('xpack.security.unauthenticated.errorDescription', { - defaultMessage: - "We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator.", - }), - }, - ], -]); +const loginFormMessages: Record> = { + SESSION_EXPIRED: { + type: LoginFormMessageType.Info, + content: i18n.translate('xpack.security.login.sessionExpiredDescription', { + defaultMessage: 'Your session has timed out. Please log in again.', + }), + }, + AUTHENTICATION_ERROR: { + type: LoginFormMessageType.Info, + content: i18n.translate('xpack.security.login.authenticationErrorDescription', { + defaultMessage: 'An unexpected authentication error occurred. Please log in again.', + }), + }, + LOGGED_OUT: { + type: LoginFormMessageType.Info, + content: i18n.translate('xpack.security.login.loggedOutDescription', { + defaultMessage: 'You have logged out of Elastic.', + }), + }, + UNAUTHENTICATED: { + type: LoginFormMessageType.Danger, + content: i18n.translate('xpack.security.unauthenticated.errorDescription', { + defaultMessage: + "We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator.", + }), + }, +}; export class LoginPage extends Component { state = { loginState: null } as State; @@ -77,7 +75,7 @@ export class LoginPage extends Component { try { this.setState({ loginState: await this.props.http.get('/internal/security/login_state') }); } catch (err) { - this.props.fatalErrors.add(err); + this.props.fatalErrors.add(err as Error); } loadingCount$.next(0); @@ -235,17 +233,19 @@ export class LoginPage extends Component { ); } - const query = parse(window.location.href, true).query; + const { searchParams } = new URL(window.location.href); + return ( ); }; diff --git a/x-pack/plugins/security/public/session/session_expired.mock.ts b/x-pack/plugins/security/public/session/session_expired.mock.ts index f3a0e2b88f7e..aa9134556cab 100644 --- a/x-pack/plugins/security/public/session/session_expired.mock.ts +++ b/x-pack/plugins/security/public/session/session_expired.mock.ts @@ -5,10 +5,12 @@ * 2.0. */ -import type { ISessionExpired } from './session_expired'; +import type { PublicMethodsOf } from '@kbn/utility-types'; + +import type { SessionExpired } from './session_expired'; export function createSessionExpiredMock() { return { logout: jest.fn(), - } as jest.Mocked; + } as jest.Mocked>; } diff --git a/x-pack/plugins/security/public/session/session_expired.test.ts b/x-pack/plugins/security/public/session/session_expired.test.ts index 40059722cee8..12fec1665ff0 100644 --- a/x-pack/plugins/security/public/session/session_expired.test.ts +++ b/x-pack/plugins/security/public/session/session_expired.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { LogoutReason } from '../../common/types'; import { SessionExpired } from './session_expired'; describe('#logout', () => { @@ -41,7 +42,7 @@ describe('#logout', () => { it(`redirects user to the logout URL with 'msg' and 'next' parameters`, async () => { const sessionExpired = new SessionExpired(LOGOUT_URL, TENANT); - sessionExpired.logout(); + sessionExpired.logout(LogoutReason.SESSION_EXPIRED); const next = `&next=${encodeURIComponent(CURRENT_URL)}`; await expect(window.location.assign).toHaveBeenCalledWith( @@ -49,12 +50,22 @@ describe('#logout', () => { ); }); + it(`redirects user to the logout URL with custom reason 'msg'`, async () => { + const sessionExpired = new SessionExpired(LOGOUT_URL, TENANT); + sessionExpired.logout(LogoutReason.AUTHENTICATION_ERROR); + + const next = `&next=${encodeURIComponent(CURRENT_URL)}`; + await expect(window.location.assign).toHaveBeenCalledWith( + `${LOGOUT_URL}?msg=AUTHENTICATION_ERROR${next}` + ); + }); + it(`adds 'provider' parameter when sessionStorage contains the provider name for this tenant`, async () => { const providerName = 'basic'; mockGetItem.mockReturnValueOnce(providerName); const sessionExpired = new SessionExpired(LOGOUT_URL, TENANT); - sessionExpired.logout(); + sessionExpired.logout(LogoutReason.SESSION_EXPIRED); expect(mockGetItem).toHaveBeenCalledTimes(1); expect(mockGetItem).toHaveBeenCalledWith(`${TENANT}/session_provider`); diff --git a/x-pack/plugins/security/public/session/session_expired.ts b/x-pack/plugins/security/public/session/session_expired.ts index 0bfbde1f31b3..ad1d4658817b 100644 --- a/x-pack/plugins/security/public/session/session_expired.ts +++ b/x-pack/plugins/security/public/session/session_expired.ts @@ -10,10 +10,7 @@ import { LOGOUT_REASON_QUERY_STRING_PARAMETER, NEXT_URL_QUERY_STRING_PARAMETER, } from '../../common/constants'; - -export interface ISessionExpired { - logout(): void; -} +import type { LogoutReason } from '../../common/types'; const getNextParameter = () => { const { location } = window; @@ -32,11 +29,11 @@ const getProviderParameter = (tenant: string) => { export class SessionExpired { constructor(private logoutUrl: string, private tenant: string) {} - logout() { + logout(reason: LogoutReason) { const next = getNextParameter(); const provider = getProviderParameter(this.tenant); window.location.assign( - `${this.logoutUrl}?${LOGOUT_REASON_QUERY_STRING_PARAMETER}=SESSION_EXPIRED${next}${provider}` + `${this.logoutUrl}?${LOGOUT_REASON_QUERY_STRING_PARAMETER}=${reason}${next}${provider}` ); } } diff --git a/x-pack/plugins/security/public/session/session_timeout.ts b/x-pack/plugins/security/public/session/session_timeout.ts index 7a23f1bb25ad..8b83f34f642f 100644 --- a/x-pack/plugins/security/public/session/session_timeout.ts +++ b/x-pack/plugins/security/public/session/session_timeout.ts @@ -24,9 +24,10 @@ import { SESSION_GRACE_PERIOD_MS, SESSION_ROUTE, } from '../../common/constants'; +import { LogoutReason } from '../../common/types'; import type { SessionInfo } from '../../common/types'; import { createSessionExpirationToast } from './session_expiration_toast'; -import type { ISessionExpired } from './session_expired'; +import type { SessionExpired } from './session_expired'; export interface SessionState extends Pick { lastExtensionTime: number; @@ -58,7 +59,7 @@ export class SessionTimeout { constructor( private notifications: NotificationsSetup, - private sessionExpired: ISessionExpired, + private sessionExpired: Pick, private http: HttpSetup, private tenant: string ) {} @@ -168,7 +169,10 @@ export class SessionTimeout { const fetchSessionInMs = showWarningInMs - SESSION_CHECK_MS; // Schedule logout when session is about to expire - this.stopLogoutTimer = startTimer(() => this.sessionExpired.logout(), logoutInMs); + this.stopLogoutTimer = startTimer( + () => this.sessionExpired.logout(LogoutReason.SESSION_EXPIRED), + logoutInMs + ); // Hide warning if session has been extended if (showWarningInMs > 0) { diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts index 35bdb911b6ed..6d955bb5ad89 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts @@ -56,6 +56,7 @@ it(`logs out 401 responses`, async () => { await drainPromiseQueue(); expect(fetchResolved).toBe(false); expect(fetchRejected).toBe(false); + expect(sessionExpired.logout).toHaveBeenCalledWith('AUTHENTICATION_ERROR'); }); it(`ignores anonymous paths`, async () => { diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts index 43945c8f43c0..92c5c4485bca 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts @@ -12,6 +12,7 @@ import type { IHttpInterceptController, } from 'src/core/public'; +import { LogoutReason } from '../../common/types'; import type { SessionExpired } from './session_expired'; export class UnauthorizedResponseHttpInterceptor implements HttpInterceptor { @@ -39,7 +40,7 @@ export class UnauthorizedResponseHttpInterceptor implements HttpInterceptor { } if (response.status === 401) { - this.sessionExpired.logout(); + this.sessionExpired.logout(LogoutReason.AUTHENTICATION_ERROR); controller.halt(); } } diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 3be565d59a11..98f11d56853b 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -1729,7 +1729,7 @@ describe('createConfig()', () => { }, }, }) - ).toThrow('[audit.appender.2.type]: expected value to equal [legacy-appender]'); + ).toThrow('[audit.appender.1.layout]: expected at least one defined value but got [undefined]'); }); it('rejects an ignore_filter when no appender is configured', () => { diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 4316b1c033ec..9ac24d96d329 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -7,7 +7,7 @@ import type { TransformConfigSchema } from './transforms/types'; import { ENABLE_CASE_CONNECTOR } from '../../cases/common'; -import { metadataTransformPattern } from './endpoint/constants'; +import { METADATA_TRANSFORMS_PATTERN } from './endpoint/constants'; export const APP_ID = 'securitySolution'; export const CASES_FEATURE_ID = 'securitySolutionCases'; @@ -247,6 +247,7 @@ export const DETECTION_ENGINE_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/ export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status`; export const DETECTION_ENGINE_RULES_BULK_ACTION = `${DETECTION_ENGINE_RULES_URL}/_bulk_action`; +export const TIMELINE_RESOLVE_URL = '/api/timeline/resolve'; export const TIMELINE_URL = '/api/timeline'; export const TIMELINES_URL = '/api/timelines'; export const TIMELINE_FAVORITE_URL = '/api/timeline/_favorite'; @@ -331,6 +332,23 @@ export const showAllOthersBucket: string[] = [ */ export const ELASTIC_NAME = 'estc'; -export const TRANSFORM_STATS_URL = `/api/transform/transforms/${metadataTransformPattern}-*/_stats`; +export const METADATA_TRANSFORM_STATS_URL = `/api/transform/transforms/${METADATA_TRANSFORMS_PATTERN}/_stats`; export const RISKY_HOSTS_INDEX = 'ml_host_risk_score_latest'; + +export const TRANSFORM_STATES = { + ABORTING: 'aborting', + FAILED: 'failed', + INDEXING: 'indexing', + STARTED: 'started', + STOPPED: 'stopped', + STOPPING: 'stopping', + WAITING: 'waiting', +}; + +export const WARNING_TRANSFORM_STATES = new Set([ + TRANSFORM_STATES.ABORTING, + TRANSFORM_STATES.FAILED, + TRANSFORM_STATES.STOPPED, + TRANSFORM_STATES.STOPPING, +]); diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index a38266c414e6..c7949299c68d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -20,10 +20,13 @@ export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current_*' /** The metadata Transform Name prefix with NO (package) version) */ export const metadataTransformPrefix = 'endpoint.metadata_current-default'; -/** The metadata Transform Name prefix with NO namespace and NO (package) version) */ -export const metadataTransformPattern = 'endpoint.metadata_current-*'; +// metadata transforms pattern for matching all metadata transform ids +export const METADATA_TRANSFORMS_PATTERN = 'endpoint.metadata_*'; +// united metadata transform id export const METADATA_UNITED_TRANSFORM = 'endpoint.metadata_united-default'; + +// united metadata transform destination index export const METADATA_UNITED_INDEX = '.metrics-endpoint.metadata_united_default'; export const policyIndexPattern = 'metrics-endpoint.policy-*'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index cc676541e2c2..c0046f7535db 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -337,21 +337,6 @@ export const TimelineIdLiteralRt = runtimeTypes.union([ export type TimelineIdLiteral = 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({ @@ -379,6 +364,33 @@ export const SingleTimelineResponseType = runtimeTypes.type({ export type SingleTimelineResponse = runtimeTypes.TypeOf; +/** Resolved Timeline Response */ +export const ResolvedTimelineSavedObjectToReturnObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + timeline: TimelineSavedToReturnObjectRuntimeType, + outcome: runtimeTypes.union([ + runtimeTypes.literal('exactMatch'), + runtimeTypes.literal('aliasMatch'), + runtimeTypes.literal('conflict'), + ]), + }), + runtimeTypes.partial({ + alias_target_id: runtimeTypes.string, + }), +]); + +export type ResolvedTimelineWithOutcomeSavedObject = runtimeTypes.TypeOf< + typeof ResolvedTimelineSavedObjectToReturnObjectRuntimeType +>; + +export const ResolvedSingleTimelineResponseType = runtimeTypes.type({ + data: ResolvedTimelineSavedObjectToReturnObjectRuntimeType, +}); + +export type SingleTimelineResolveResponse = runtimeTypes.TypeOf< + typeof ResolvedSingleTimelineResponseType +>; + /** * All Timeline Saved object type with metadata */ diff --git a/x-pack/plugins/security_solution/cypress/README.md b/x-pack/plugins/security_solution/cypress/README.md index d70011f86486..b500091aacc2 100644 --- a/x-pack/plugins/security_solution/cypress/README.md +++ b/x-pack/plugins/security_solution/cypress/README.md @@ -207,7 +207,7 @@ node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solut # launch the cypress test runner with overridden environment variables cd x-pack/plugins/security_solution -CYPRESS_base_url=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= CYPRESS_protocol= CYPRESS_hostname= CYPRESS_configport= CYPRESS_KIBANA_URL= yarn cypress:run +CYPRESS_BASE_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:run ``` #### Custom Target + Headless (Firefox) @@ -225,7 +225,7 @@ node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solut # launch the cypress test runner with overridden environment variables cd x-pack/plugins/security_solution -CYPRESS_base_url=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= CYPRESS_protocol= CYPRESS_hostname= CYPRESS_configport= CYPRESS_KIBANA_URL= yarn cypress:run:firefox +CYPRESS_BASE_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:run:firefox ``` #### CCS Custom Target + Headless diff --git a/x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts new file mode 100644 index 000000000000..c20e6cf6b637 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { esArchiverCCSLoad } from '../../tasks/es_archiver'; +import { getCCSEqlRule } from '../../objects/rule'; + +import { ALERT_DATA_GRID, NUMBER_OF_ALERTS } from '../../screens/alerts'; + +import { + filterByCustomRules, + goToRuleDetails, + waitForRulesTableToBeLoaded, +} from '../../tasks/alerts_detection_rules'; +import { createSignalsIndex, createEventCorrelationRule } from '../../tasks/api_calls/rules'; +import { cleanKibana } from '../../tasks/common'; +import { waitForAlertsToPopulate, waitForTheRuleToBeExecuted } from '../../tasks/create_new_rule'; +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; + +describe('Detection rules', function () { + const expectedNumberOfAlerts = '1 alert'; + + beforeEach('Reset signals index', function () { + cleanKibana(); + createSignalsIndex(); + }); + + it('EQL rule on remote indices generates alerts', function () { + esArchiverCCSLoad('linux_process'); + this.rule = getCCSEqlRule(); + createEventCorrelationRule(this.rule); + + loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + waitForRulesTableToBeLoaded(); + filterByCustomRules(); + goToRuleDetails(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); + cy.get(ALERT_DATA_GRID) + .invoke('text') + .then((text) => { + cy.log('ALERT_DATA_GRID', text); + expect(text).contains(this.rule.name); + expect(text).contains(this.rule.severity.toLowerCase()); + expect(text).contains(this.rule.riskScore); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts index 3930088f8bfd..48269677466b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts @@ -51,12 +51,11 @@ import { TIMELINES_URL } from '../../urls/navigation'; describe('Timeline Templates', () => { beforeEach(() => { cleanKibana(); - loginAndWaitForPageWithoutDateRange(TIMELINES_URL); - cy.intercept('PATCH', '/api/timeline').as('timeline'); }); it('Creates a timeline template', async () => { + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); openTimelineUsingToggle(); createNewTimelineTemplate(); populateTimeline(); @@ -103,20 +102,15 @@ describe('Timeline Templates', () => { }); it('Create template from timeline', () => { + createTimeline(getTimeline()); + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); waitForTimelinesPanelToBeLoaded(); + expandEventAction(); + clickingOnCreateTemplateFromTimelineBtn(); - createTimeline(getTimeline()).then(() => { - expandEventAction(); - clickingOnCreateTemplateFromTimelineBtn(); - cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => { - expect(request.body.timeline).to.haveOwnProperty('templateTimelineId'); - expect(request.body.timeline).to.haveOwnProperty('description', getTimeline().description); - expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty( - 'expression', - getTimeline().query - ); - cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); - }); - }); + cy.wait('@timeline', { timeout: 100000 }); + cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); + cy.get(TIMELINE_DESCRIPTION).should('have.text', getTimeline().description); + cy.get(TIMELINE_QUERY).should('have.text', getTimeline().query); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts index 6b86957c78e6..fb41aec91b6c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts @@ -23,9 +23,9 @@ import { import { createTimelineTemplate } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; - import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; +import { selectCustomTemplates } from '../../tasks/templates'; import { addEqlToTimeline, addFilter, @@ -115,22 +115,22 @@ describe('Timelines', (): void => { describe('Create a timeline from a template', () => { before(() => { + cy.intercept('/api/timeline*').as('timeline'); cleanKibana(); + createTimelineTemplate(getTimeline()); loginAndWaitForPageWithoutDateRange(TIMELINE_TEMPLATES_URL); waitForTimelinesPanelToBeLoaded(); }); it('Should have the same query and open the timeline modal', () => { - createTimelineTemplate(getTimeline()).then(() => { - expandEventAction(); - cy.intercept('/api/timeline').as('timeline'); - - clickingOnCreateTimelineFormTemplateBtn(); - cy.wait('@timeline', { timeout: 100000 }); - - cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); - cy.get(TIMELINE_DESCRIPTION).should('have.text', getTimeline().description); - cy.get(TIMELINE_QUERY).should('have.text', getTimeline().query); - }); + selectCustomTemplates(); + cy.wait('@timeline', { timeout: 100000 }); + expandEventAction(); + clickingOnCreateTimelineFormTemplateBtn(); + cy.wait('@timeline', { timeout: 100000 }); + + cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); + cy.get(TIMELINE_DESCRIPTION).should('have.text', getTimeline().description); + cy.get(TIMELINE_QUERY).should('have.text', getTimeline().query); }); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 173bfa524e66..db76bfc3cf4d 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -72,6 +72,10 @@ export interface OverrideRule extends CustomRule { timestampOverride: string; } +export interface EventCorrelationRule extends CustomRule { + language: string; +} + export interface ThreatIndicatorRule extends CustomRule { indicatorIndexPattern: string[]; indicatorMappingField: string; @@ -326,6 +330,25 @@ export const getEqlRule = (): CustomRule => ({ maxSignals: 100, }); +export const getCCSEqlRule = (): EventCorrelationRule => ({ + customQuery: 'any where process.name == "run-parts"', + name: 'New EQL Rule', + index: [`${ccsRemoteName}:run-parts`], + description: 'New EQL rule description.', + severity: 'High', + riskScore: '17', + tags: ['test', 'newRule'], + referenceUrls: ['http://example.com/', 'https://example.com/'], + falsePositivesExamples: ['False1', 'False2'], + mitre: [getMitre1(), getMitre2()], + note: '# test markdown', + runsEvery: getRunsEvery(), + lookBack: getLookBack(), + timeline: getTimeline(), + maxSignals: 100, + language: 'eql', +}); + export const getEqlSequenceRule = (): CustomRule => ({ customQuery: 'sequence with maxspan=30s\ diff --git a/x-pack/plugins/security_solution/cypress/screens/templates.ts b/x-pack/plugins/security_solution/cypress/screens/templates.ts new file mode 100644 index 000000000000..65fab879b143 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/templates.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const CUSTOM_TEMPLATES = '[data-test-subj="Custom templates"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 7d2116cff7bf..6b985c7009b2 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -72,15 +72,13 @@ export const duplicateFirstRule = () => { * flake. */ export const duplicateRuleFromMenu = () => { + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click({ force: true }); cy.get(LOADING_INDICATOR).should('not.exist'); - cy.root() - .pipe(($el) => { - $el.find(ALL_ACTIONS).trigger('click'); - return $el.find(DUPLICATE_RULE_MENU_PANEL_BTN); - }) - .should(($el) => expect($el).to.be.visible); + cy.get(ALL_ACTIONS).pipe(click); + cy.get(DUPLICATE_RULE_MENU_PANEL_BTN).should('be.visible'); + // Because of a fade effect and fast clicking this can produce more than one click - cy.get(DUPLICATE_RULE_MENU_PANEL_BTN).pipe(($el) => $el.trigger('click')); + cy.get(DUPLICATE_RULE_MENU_PANEL_BTN).pipe(click); }; /** diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 33bd8a06b998..130467cde053 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CustomRule, ThreatIndicatorRule } from '../../objects/rule'; +import { CustomRule, EventCorrelationRule, ThreatIndicatorRule } from '../../objects/rule'; export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', interval = '100m') => cy.request({ @@ -29,6 +29,27 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte failOnStatusCode: false, }); +export const createEventCorrelationRule = (rule: EventCorrelationRule, ruleId = 'rule_testing') => + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, + from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'eql', + index: rule.index, + query: rule.customQuery, + language: 'eql', + enabled: true, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); + export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') => cy.request({ method: 'POST', @@ -107,6 +128,14 @@ export const deleteCustomRule = (ruleId = '1') => { }); }; +export const createSignalsIndex = () => { + cy.request({ + method: 'POST', + url: 'api/detection_engine/index', + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); +}; + export const removeSignalsIndex = () => { cy.request({ url: '/api/detection_engine/index', failOnStatusCode: false }).then((response) => { if (response.status === 200) { diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index 243bfd113bfd..5a935702131d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -56,13 +56,15 @@ const LOGIN_API_ENDPOINT = '/internal/security/login'; * @param route string route to visit */ export const getUrlWithRoute = (role: ROLES, route: string) => { + const url = Cypress.config().baseUrl; + const kibana = new URL(url!); const theUrl = `${Url.format({ auth: `${role}:changeme`, username: role, password: 'changeme', - protocol: Cypress.env('protocol'), - hostname: Cypress.env('hostname'), - port: Cypress.env('configport'), + protocol: kibana.protocol.replace(':', ''), + hostname: kibana.hostname, + port: kibana.port, } as UrlObject)}${route.startsWith('/') ? '' : '/'}${route}`; cy.log(`origin: ${theUrl}`); return theUrl; @@ -80,11 +82,13 @@ interface User { * @param route string route to visit */ export const constructUrlWithUser = (user: User, route: string) => { - const hostname = Cypress.env('hostname'); + const url = Cypress.config().baseUrl; + const kibana = new URL(url!); + const hostname = kibana.hostname; const username = user.username; const password = user.password; - const protocol = Cypress.env('protocol'); - const port = Cypress.env('configport'); + const protocol = kibana.protocol.replace(':', ''); + const port = kibana.port; const path = `${route.startsWith('/') ? '' : '/'}${route}`; const strUrl = `${protocol}://${username}:${password}@${hostname}:${port}${path}`; @@ -98,7 +102,7 @@ export const getCurlScriptEnvVars = () => ({ ELASTICSEARCH_URL: Cypress.env('ELASTICSEARCH_URL'), ELASTICSEARCH_USERNAME: Cypress.env('ELASTICSEARCH_USERNAME'), ELASTICSEARCH_PASSWORD: Cypress.env('ELASTICSEARCH_PASSWORD'), - KIBANA_URL: Cypress.env('KIBANA_URL'), + KIBANA_URL: Cypress.config().baseUrl, }); export const postRoleAndUser = (role: ROLES) => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/templates.ts b/x-pack/plugins/security_solution/cypress/tasks/templates.ts new file mode 100644 index 000000000000..6490a7c1df71 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/templates.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CUSTOM_TEMPLATES } from '../screens/templates'; + +export const selectCustomTemplates = () => { + cy.get(CUSTOM_TEMPLATES).click({ force: true }); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 03ccb784bd25..039e8ed44886 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -390,5 +390,10 @@ export const clickingOnCreateTemplateFromTimelineBtn = () => { }; export const expandEventAction = () => { - cy.get(TIMELINE_COLLAPSED_ITEMS_BTN).first().click(); + cy.waitUntil(() => { + cy.get(TIMELINE_COLLAPSED_ITEMS_BTN).should('exist'); + cy.get(TIMELINE_COLLAPSED_ITEMS_BTN).should('be.visible'); + return cy.get(TIMELINE_COLLAPSED_ITEMS_BTN).then(($el) => $el.length === 1); + }); + cy.get(TIMELINE_COLLAPSED_ITEMS_BTN).click(); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts index bda78955a244..a135ce8c9051 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { LOADING_INDICATOR } from '../screens/security_header'; import { TIMELINE_CHECKBOX, BULK_ACTIONS, @@ -24,6 +25,8 @@ export const openTimeline = (id: string) => { cy.get(TIMELINE(id)).should('be.visible').pipe(click); }; -export const waitForTimelinesPanelToBeLoaded = (): Cypress.Chainable> => { - return cy.get(TIMELINES_TABLE).should('exist'); +export const waitForTimelinesPanelToBeLoaded = () => { + cy.get(LOADING_INDICATOR).should('exist'); + cy.get(LOADING_INDICATOR).should('not.exist'); + cy.get(TIMELINES_TABLE).should('exist'); }; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx index 6abca9cfe885..cf5e8a5bad80 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx @@ -14,7 +14,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; -import React, { Dispatch, useCallback, useReducer, useState } from 'react'; +import React, { Dispatch, useCallback, useReducer, useState, useMemo } from 'react'; import styled from 'styled-components'; import { useKibana } from '../../lib/kibana'; @@ -30,6 +30,7 @@ import * as i18n from './translations'; import { JobsFilters, SecurityJob } from './types'; import { UpgradeContents } from './upgrade_contents'; import { useSecurityJobs } from './hooks/use_security_jobs'; +import { MLJobsAwaitingNodeWarning } from '../../../../../ml/public'; const PopoverContentsDiv = styled.div` max-width: 684px; @@ -116,6 +117,10 @@ export const MlPopover = React.memo(() => { }); const incompatibleJobCount = jobs.filter((j) => !j.isCompatible).length; + const installedJobsIds = useMemo( + () => jobs.filter((j) => j.isInstalled).map((j) => j.id), + [jobs] + ); if (!isLicensed) { // If the user does not have platinum show upgrade UI @@ -216,6 +221,7 @@ export const MlPopover = React.memo(() => { )} + HostResultList; @@ -238,14 +238,14 @@ export const failedTransformStateMock = { count: 1, transforms: [ { - state: TRANSFORM_STATE.FAILED, + state: TRANSFORM_STATES.FAILED, }, ], }; export const transformsHttpMocks = httpHandlerMockFactory([ { id: 'metadataTransformStats', - path: TRANSFORM_STATS_URL, + path: METADATA_TRANSFORM_STATS_URL, method: 'get', handler: () => failedTransformStateMock, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 84cf3513d5d3..7a45ff06c496 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -78,7 +78,7 @@ import { resolvePathVariables } from '../../../../common/utils/resolve_path_vari import { EndpointPackageInfoStateChanged } from './action'; import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions'; import { getIsInvalidDateRange } from '../utils'; -import { TRANSFORM_STATS_URL } from '../../../../../common/constants'; +import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants'; type EndpointPageStore = ImmutableMiddlewareAPI; @@ -785,7 +785,9 @@ export async function handleLoadMetadataTransformStats(http: HttpStart, store: E }); try { - const transformStatsResponse: TransformStatsResponse = await http.get(TRANSFORM_STATS_URL); + const transformStatsResponse: TransformStatsResponse = await http.get( + METADATA_TRANSFORM_STATS_URL + ); dispatch({ type: 'metadataTransformStatsChanged', diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index 8e8e5a61221a..2e3de427e696 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -30,7 +30,7 @@ import { import { GetPolicyListResponse } from '../../policy/types'; import { pendingActionsResponseMock } from '../../../../common/lib/endpoint_pending_actions/mocks'; import { ACTION_STATUS_ROUTE } from '../../../../../common/endpoint/constants'; -import { TRANSFORM_STATS_URL } from '../../../../../common/constants'; +import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants'; import { TransformStats, TransformStatsResponse } from '../types'; const generator = new EndpointDocGenerator('seed'); @@ -163,7 +163,7 @@ const endpointListApiPathHandlerMocks = ({ return pendingActionsResponseMock(); }, - [TRANSFORM_STATS_URL]: (): TransformStatsResponse => ({ + [METADATA_TRANSFORM_STATS_URL]: (): TransformStatsResponse => ({ count: transforms.length, transforms, }), diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index dd0bc79f1ba5..0fa96fe00fd2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -22,6 +22,7 @@ import { ServerApiError } from '../../../common/types'; import { GetPackagesResponse } from '../../../../../fleet/common'; import { IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { AsyncResourceState } from '../../state'; +import { TRANSFORM_STATES } from '../../../../common/constants'; export interface EndpointState { /** list of host **/ @@ -143,24 +144,7 @@ export interface EndpointIndexUIQueryParams { admin_query?: string; } -export const TRANSFORM_STATE = { - ABORTING: 'aborting', - FAILED: 'failed', - INDEXING: 'indexing', - STARTED: 'started', - STOPPED: 'stopped', - STOPPING: 'stopping', - WAITING: 'waiting', -}; - -export const WARNING_TRANSFORM_STATES = new Set([ - TRANSFORM_STATE.ABORTING, - TRANSFORM_STATE.FAILED, - TRANSFORM_STATE.STOPPED, - TRANSFORM_STATE.STOPPING, -]); - -const transformStates = Object.values(TRANSFORM_STATE); +const transformStates = Object.values(TRANSFORM_STATES); export type TransformState = typeof transformStates[number]; export interface TransformStats { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 33c45e6e2f54..b2c438659b77 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -46,8 +46,9 @@ import { APP_PATH, MANAGEMENT_PATH, DEFAULT_TIMEPICKER_QUICK_RANGES, + TRANSFORM_STATES, } from '../../../../../common/constants'; -import { TransformStats, TRANSFORM_STATE } from '../types'; +import { TransformStats } from '../types'; import { metadataTransformPrefix } from '../../../../../common/endpoint/constants'; // not sure why this can't be imported from '../../../../common/mock/formatted_relative'; @@ -1403,7 +1404,7 @@ describe('when on the endpoint list page', () => { const transforms: TransformStats[] = [ { id: `${metadataTransformPrefix}-0.20.0`, - state: TRANSFORM_STATE.STARTED, + state: TRANSFORM_STATES.STARTED, } as TransformStats, ]; setEndpointListApiMockImplementation(coreStart.http, { transforms }); @@ -1414,7 +1415,7 @@ describe('when on the endpoint list page', () => { it('is not displayed when non-relevant transform is failing', () => { const transforms: TransformStats[] = [ - { id: 'not-metadata', state: TRANSFORM_STATE.FAILED } as TransformStats, + { id: 'not-metadata', state: TRANSFORM_STATES.FAILED } as TransformStats, ]; setEndpointListApiMockImplementation(coreStart.http, { transforms }); render(); @@ -1426,7 +1427,7 @@ describe('when on the endpoint list page', () => { const transforms: TransformStats[] = [ { id: `${metadataTransformPrefix}-0.20.0`, - state: TRANSFORM_STATE.FAILED, + state: TRANSFORM_STATES.FAILED, } as TransformStats, ]; setEndpointListApiMockImplementation(coreStart.http, { transforms }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index e71474321c86..784540935389 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -58,8 +58,8 @@ import { LinkToApp } from '../../../../common/components/endpoint/link_to_app'; import { TableRowActions } from './components/table_row_actions'; import { EndpointAgentStatus } from './components/endpoint_agent_status'; import { CallOut } from '../../../../common/components/callouts'; -import { WARNING_TRANSFORM_STATES } from '../types'; import { metadataTransformPrefix } from '../../../../../common/endpoint/constants'; +import { WARNING_TRANSFORM_STATES } from '../../../../../common/constants'; const MAX_PAGINATED_ITEM = 9999; const TRANSFORM_URL = '/data/transform'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx index d1f95496bb70..15f97b72855f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx @@ -62,6 +62,7 @@ export const SearchRow = React.memo( ? i18n.SEARCH_PLACEHOLDER : i18n.SEARCH_TEMPLATE_PLACEHOLDER, incremental: false, + 'data-test-subj': 'search-bar', }), [timelineType] ); @@ -70,7 +71,7 @@ export const SearchRow = React.memo( - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx index 060212d0972d..097919d7e6f0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx @@ -97,6 +97,7 @@ export const useTimelineStatus = ({ onClick={onFilterClicked.bind(null, tab.id)} withNext={tab.withNext} isDisabled={tab.disabled} + data-test-subj={tab.name} > {tab.name} diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts index 171b3fedd0e6..789c942d0e29 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts @@ -22,11 +22,13 @@ import { ResponseFavoriteTimeline, AllTimelinesResponse, SingleTimelineResponse, + SingleTimelineResolveResponse, allTimelinesResponse, responseFavoriteTimeline, GetTimelinesArgs, SingleTimelineResponseType, TimelineType, + ResolvedSingleTimelineResponseType, } from '../../../common/types/timeline'; import { TIMELINE_URL, @@ -34,6 +36,7 @@ import { TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL, TIMELINE_PREPACKAGED_URL, + TIMELINE_RESOLVE_URL, TIMELINES_URL, TIMELINE_FAVORITE_URL, } from '../../../common/constants'; @@ -71,6 +74,12 @@ const decodeSingleTimelineResponse = (respTimeline?: SingleTimelineResponse) => fold(throwErrors(createToasterPlainError), identity) ); +const decodeResolvedSingleTimelineResponse = (respTimeline?: SingleTimelineResolveResponse) => + pipe( + ResolvedSingleTimelineResponseType.decode(respTimeline), + fold(throwErrors(createToasterPlainError), identity) + ); + const decodeAllTimelinesResponse = (respTimeline: AllTimelinesResponse) => pipe( allTimelinesResponse.decode(respTimeline), @@ -305,6 +314,19 @@ export const getTimeline = async (id: string) => { return decodeSingleTimelineResponse(response); }; +export const resolveTimeline = async (id: string) => { + const response = await KibanaServices.get().http.get( + TIMELINE_RESOLVE_URL, + { + query: { + id, + }, + } + ); + + return decodeResolvedSingleTimelineResponse(response); +}; + export const getTimelineTemplate = async (templateTimelineId: string) => { const response = await KibanaServices.get().http.get(TIMELINE_URL, { query: { @@ -315,6 +337,19 @@ export const getTimelineTemplate = async (templateTimelineId: string) => { return decodeSingleTimelineResponse(response); }; +export const getResolvedTimelineTemplate = async (templateTimelineId: string) => { + const response = await KibanaServices.get().http.get( + TIMELINE_RESOLVE_URL, + { + query: { + template_timeline_id: templateTimelineId, + }, + } + ); + + return decodeResolvedSingleTimelineResponse(response); +}; + export const getAllTimelines = async (args: GetTimelinesArgs, abortSignal: AbortSignal) => { const response = await KibanaServices.get().http.fetch(TIMELINES_URL, { method: 'GET', diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts new file mode 100644 index 000000000000..0510743fdf05 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts @@ -0,0 +1,250 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ApiResponse } from '@elastic/elasticsearch'; +import { TransformGetTransformStatsResponse } from '@elastic/elasticsearch/api/types'; +import { + CheckMetadataTransformsTask, + TYPE, + VERSION, + BASE_NEXT_ATTEMPT_DELAY, +} from './check_metadata_transforms_task'; +import { createMockEndpointAppContext } from '../../mocks'; +import { coreMock } from '../../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../../task_manager/server/mocks'; +import { TaskManagerSetupContract, TaskStatus } from '../../../../../task_manager/server'; +import { CoreSetup } from '../../../../../../../src/core/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ElasticsearchClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks'; +import { TRANSFORM_STATES } from '../../../../common/constants'; +import { METADATA_TRANSFORMS_PATTERN } from '../../../../common/endpoint/constants'; +import { RunResult } from '../../../../../task_manager/server/task'; + +const MOCK_TASK_INSTANCE = { + id: `${TYPE}:${VERSION}`, + runAt: new Date(), + attempts: 0, + ownerId: '', + status: TaskStatus.Running, + startedAt: new Date(), + scheduledAt: new Date(), + retryAt: new Date(), + params: {}, + state: {}, + taskType: TYPE, +}; +const failedTransformId = 'failing-transform'; +const goodTransformId = 'good-transform'; + +describe('check metadata transforms task', () => { + const { createSetup: coreSetupMock } = coreMock; + const { createSetup: tmSetupMock, createStart: tmStartMock } = taskManagerMock; + + let mockTask: CheckMetadataTransformsTask; + let mockCore: CoreSetup; + let mockTaskManagerSetup: jest.Mocked; + beforeAll(() => { + mockCore = coreSetupMock(); + mockTaskManagerSetup = tmSetupMock(); + mockTask = new CheckMetadataTransformsTask({ + endpointAppContext: createMockEndpointAppContext(), + core: mockCore, + taskManager: mockTaskManagerSetup, + }); + }); + + describe('task lifecycle', () => { + it('should create task', () => { + expect(mockTask).toBeInstanceOf(CheckMetadataTransformsTask); + }); + + it('should register task', () => { + expect(mockTaskManagerSetup.registerTaskDefinitions).toHaveBeenCalled(); + }); + + it('should schedule task', async () => { + const mockTaskManagerStart = tmStartMock(); + await mockTask.start({ taskManager: mockTaskManagerStart }); + expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalled(); + }); + }); + + describe('task logic', () => { + let esClient: ElasticsearchClientMock; + beforeEach(async () => { + const [{ elasticsearch }] = await mockCore.getStartServices(); + esClient = elasticsearch.client.asInternalUser as ElasticsearchClientMock; + }); + + const runTask = async (taskInstance = MOCK_TASK_INSTANCE) => { + const mockTaskManagerStart = tmStartMock(); + await mockTask.start({ taskManager: mockTaskManagerStart }); + const createTaskRunner = + mockTaskManagerSetup.registerTaskDefinitions.mock.calls[0][0][TYPE].createTaskRunner; + const taskRunner = createTaskRunner({ taskInstance }); + return taskRunner.run(); + }; + + const buildFailedStatsResponse = () => + ({ + body: { + transforms: [ + { + id: goodTransformId, + state: TRANSFORM_STATES.STARTED, + }, + { + id: failedTransformId, + state: TRANSFORM_STATES.FAILED, + }, + ], + }, + } as unknown as ApiResponse); + + it('should stop task if transform stats response fails', async () => { + esClient.transform.getTransformStats.mockRejectedValue({}); + await runTask(); + expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({ + transform_id: METADATA_TRANSFORMS_PATTERN, + }); + expect(esClient.transform.stopTransform).not.toHaveBeenCalled(); + expect(esClient.transform.startTransform).not.toHaveBeenCalled(); + }); + + it('should attempt transform restart if failing state', async () => { + const transformStatsResponseMock = buildFailedStatsResponse(); + esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); + + const taskResponse = (await runTask()) as RunResult; + + expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({ + transform_id: METADATA_TRANSFORMS_PATTERN, + }); + expect(esClient.transform.stopTransform).toHaveBeenCalledWith({ + transform_id: failedTransformId, + allow_no_match: true, + wait_for_completion: true, + force: true, + }); + expect(esClient.transform.startTransform).toHaveBeenCalledWith({ + transform_id: failedTransformId, + }); + expect(taskResponse?.state?.attempts).toEqual({ + [goodTransformId]: 0, + [failedTransformId]: 0, + }); + }); + + it('should correctly track transform restart attempts', async () => { + const transformStatsResponseMock = buildFailedStatsResponse(); + esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); + + esClient.transform.stopTransform.mockRejectedValueOnce({}); + let taskResponse = (await runTask()) as RunResult; + expect(taskResponse?.state?.attempts).toEqual({ + [goodTransformId]: 0, + [failedTransformId]: 1, + }); + + esClient.transform.startTransform.mockRejectedValueOnce({}); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + expect(taskResponse?.state?.attempts).toEqual({ + [goodTransformId]: 0, + [failedTransformId]: 2, + }); + + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + expect(taskResponse?.state?.attempts).toEqual({ + [goodTransformId]: 0, + [failedTransformId]: 0, + }); + }); + + it('should correctly back off subsequent restart attempts', async () => { + let transformStatsResponseMock = buildFailedStatsResponse(); + esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); + + esClient.transform.stopTransform.mockRejectedValueOnce({}); + let taskStartedAt = new Date(); + let taskResponse = (await runTask()) as RunResult; + let delay = BASE_NEXT_ATTEMPT_DELAY * 60000; + let expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + let expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + esClient.transform.startTransform.mockRejectedValueOnce({}); + taskStartedAt = new Date(); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + // should be exponential on second+ attempt + delay = BASE_NEXT_ATTEMPT_DELAY ** 2 * 60000; + expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + esClient.transform.stopTransform.mockRejectedValueOnce({}); + taskStartedAt = new Date(); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + // should be exponential on second+ attempt + delay = BASE_NEXT_ATTEMPT_DELAY ** 3 * 60000; + expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + taskStartedAt = new Date(); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + // back to base delay after success + delay = BASE_NEXT_ATTEMPT_DELAY * 60000; + expectedRunAt = taskStartedAt.getTime() + delay; + expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt); + // we don't have the exact timestamp it uses so give a buffer + expectedRunAtUpperBound = expectedRunAt + 1000; + expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound); + + transformStatsResponseMock = { + body: { + transforms: [ + { + id: goodTransformId, + state: TRANSFORM_STATES.STARTED, + }, + { + id: failedTransformId, + state: TRANSFORM_STATES.STARTED, + }, + ], + }, + } as unknown as ApiResponse; + esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock); + taskResponse = (await runTask({ + ...MOCK_TASK_INSTANCE, + state: taskResponse.state, + })) as RunResult; + // no more explicit runAt after subsequent success + expect(taskResponse?.runAt).toBeUndefined(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts new file mode 100644 index 000000000000..68f149bcc64c --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ApiResponse } from '@elastic/elasticsearch'; +import { + TransformGetTransformStatsResponse, + TransformGetTransformStatsTransformStats, +} from '@elastic/elasticsearch/api/types'; +import { CoreSetup, ElasticsearchClient, Logger } from 'src/core/server'; +import { + ConcreteTaskInstance, + TaskManagerSetupContract, + TaskManagerStartContract, + throwUnrecoverableError, +} from '../../../../../task_manager/server'; +import { EndpointAppContext } from '../../types'; +import { METADATA_TRANSFORMS_PATTERN } from '../../../../common/endpoint/constants'; +import { WARNING_TRANSFORM_STATES } from '../../../../common/constants'; +import { wrapErrorIfNeeded } from '../../utils'; + +const SCOPE = ['securitySolution']; +const INTERVAL = '2h'; +const TIMEOUT = '4m'; +export const TYPE = 'endpoint:metadata-check-transforms-task'; +export const VERSION = '0.0.1'; +const MAX_ATTEMPTS = 5; +export const BASE_NEXT_ATTEMPT_DELAY = 5; // minutes + +export interface CheckMetadataTransformsTaskSetupContract { + endpointAppContext: EndpointAppContext; + core: CoreSetup; + taskManager: TaskManagerSetupContract; +} + +export interface CheckMetadataTransformsTaskStartContract { + taskManager: TaskManagerStartContract; +} + +export class CheckMetadataTransformsTask { + private logger: Logger; + private wasStarted: boolean = false; + + constructor(setupContract: CheckMetadataTransformsTaskSetupContract) { + const { endpointAppContext, core, taskManager } = setupContract; + this.logger = endpointAppContext.logFactory.get(this.getTaskId()); + taskManager.registerTaskDefinitions({ + [TYPE]: { + title: 'Security Solution Endpoint Metadata Periodic Tasks', + timeout: TIMEOUT, + createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { + return { + run: async () => { + return this.runTask(taskInstance, core); + }, + cancel: async () => {}, + }; + }, + }, + }); + } + + public start = async ({ taskManager }: CheckMetadataTransformsTaskStartContract) => { + if (!taskManager) { + this.logger.error('missing required service during start'); + return; + } + + this.wasStarted = true; + + try { + await taskManager.ensureScheduled({ + id: this.getTaskId(), + taskType: TYPE, + scope: SCOPE, + schedule: { + interval: INTERVAL, + }, + state: { + attempts: {}, + }, + params: { version: VERSION }, + }); + } catch (e) { + this.logger.debug(`Error scheduling task, received ${e.message}`); + } + }; + + private runTask = async (taskInstance: ConcreteTaskInstance, core: CoreSetup) => { + // if task was not `.start()`'d yet, then exit + if (!this.wasStarted) { + this.logger.debug('[runTask()] Aborted. MetadataTask not started yet'); + return; + } + + // Check that this task is current + if (taskInstance.id !== this.getTaskId()) { + // old task, die + throwUnrecoverableError(new Error('Outdated task version')); + } + + const [{ elasticsearch }] = await core.getStartServices(); + const esClient = elasticsearch.client.asInternalUser; + + let transformStatsResponse: ApiResponse; + try { + transformStatsResponse = await esClient?.transform.getTransformStats({ + transform_id: METADATA_TRANSFORMS_PATTERN, + }); + } catch (e) { + const err = wrapErrorIfNeeded(e); + const errMessage = `failed to get transform stats with error: ${err}`; + this.logger.error(errMessage); + + return; + } + + const { transforms } = transformStatsResponse.body; + if (!transforms.length) { + this.logger.info('no OLM metadata transforms found'); + return; + } + + let didAttemptRestart: boolean = false; + let highestAttempt: number = 0; + const attempts = { ...taskInstance.state.attempts }; + + for (const transform of transforms) { + const restartedTransform = await this.restartTransform( + esClient, + transform, + attempts[transform.id] + ); + if (restartedTransform.didAttemptRestart) { + didAttemptRestart = true; + } + attempts[transform.id] = restartedTransform.attempts; + highestAttempt = Math.max(attempts[transform.id], highestAttempt); + } + + // after a restart attempt run next check sooner with exponential backoff + let runAt: Date | undefined; + if (didAttemptRestart) { + const delay = BASE_NEXT_ATTEMPT_DELAY ** Math.max(highestAttempt, 1) * 60000; + runAt = new Date(new Date().getTime() + delay); + } + + const nextState = { attempts }; + const nextTask = runAt ? { state: nextState, runAt } : { state: nextState }; + return nextTask; + }; + + private restartTransform = async ( + esClient: ElasticsearchClient, + transform: TransformGetTransformStatsTransformStats, + currentAttempts: number = 0 + ) => { + let attempts = currentAttempts; + let didAttemptRestart = false; + + if (!WARNING_TRANSFORM_STATES.has(transform.state)) { + return { + attempts, + didAttemptRestart, + }; + } + + if (attempts > MAX_ATTEMPTS) { + this.logger.warn( + `transform ${transform.id} has failed to restart ${attempts} times. stopping auto restart attempts.` + ); + return { + attempts, + didAttemptRestart, + }; + } + + try { + this.logger.info(`failed transform detected with id: ${transform.id}. attempting restart.`); + await esClient.transform.stopTransform({ + transform_id: transform.id, + allow_no_match: true, + wait_for_completion: true, + force: true, + }); + await esClient.transform.startTransform({ + transform_id: transform.id, + }); + + // restart succeeded, reset attempt count + attempts = 0; + } catch (e) { + const err = wrapErrorIfNeeded(e); + const errMessage = `failed to restart transform ${transform.id} with error: ${err}`; + this.logger.error(errMessage); + + // restart failed, increment attempt count + attempts = attempts + 1; + } finally { + didAttemptRestart = true; + } + + return { + attempts, + didAttemptRestart, + }; + }; + + private getTaskId = (): string => { + return `${TYPE}:${VERSION}`; + }; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/index.ts new file mode 100644 index 000000000000..6f5d6f5a4121 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './check_metadata_transforms_task'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts index 07f571bc7be1..fa05b1fb5b07 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts @@ -6,7 +6,6 @@ */ import { Logger } from 'src/core/server'; -import { schema } from '@kbn/config-schema'; import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE, @@ -15,12 +14,19 @@ import { } from '../../../../common/constants'; // eslint-disable-next-line no-restricted-imports -import { LegacyNotificationAlertTypeDefinition } from './legacy_types'; +import { + LegacyNotificationAlertTypeDefinition, + legacyRulesNotificationParams, +} from './legacy_types'; import { AlertAttributes } from '../signals/types'; import { siemRuleActionGroups } from '../signals/siem_rule_action_groups'; import { scheduleNotificationActions } from './schedule_notification_actions'; import { getNotificationResultsLink } from './utils'; import { getSignals } from './get_signals'; +// eslint-disable-next-line no-restricted-imports +import { legacyExtractReferences } from './legacy_saved_object_references/legacy_extract_references'; +// eslint-disable-next-line no-restricted-imports +import { legacyInjectReferences } from './legacy_saved_object_references/legacy_inject_references'; /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function @@ -36,9 +42,12 @@ export const legacyRulesNotificationAlertType = ({ defaultActionGroupId: 'default', producer: SERVER_APP_ID, validate: { - params: schema.object({ - ruleAlertId: schema.string(), - }), + params: legacyRulesNotificationParams, + }, + useSavedObjectReferences: { + extractReferences: (params) => legacyExtractReferences({ logger, params }), + injectReferences: (params, savedObjectReferences) => + legacyInjectReferences({ logger, params, savedObjectReferences }), }, minimumLicenseRequired: 'basic', isExportable: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/README.md new file mode 100644 index 000000000000..da9ccd30cfda --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/README.md @@ -0,0 +1,217 @@ +This is where you add code when you have rules which contain saved object references. Saved object references are for +when you have "joins" in the saved objects between one saved object and another one. This can be a 1 to M (1 to many) +relationship for example where you have a rule which contains the "id" of another saved object. + +NOTE: This is the "legacy saved object references" and should only be for the "legacy_rules_notification_alert_type". +The legacy notification system is being phased out and deprecated in favor of using the newer alerting notification system. +It would be considered wrong to see additional code being added here at this point. However, maintenance should be expected +until we have all users moved away from the legacy system. + + +## How to create a legacy notification + +* Create a rule and activate it normally within security_solution +* Do not add actions to the rule at this point as we are exercising the older legacy system. However, you want at least one action configured such as a slack notification. +* Within dev tools do a query for all your actions and grab one of the `_id` of them without their prefix: + +```json +# See all your actions +GET .kibana/_search +{ + "query": { + "term": { + "type": "action" + } + } +} +``` + +Mine was `"_id" : "action:879e8ff0-1be1-11ec-a722-83da1c22a481"`, so I will be copying the ID of `879e8ff0-1be1-11ec-a722-83da1c22a481` + +Go to the file `detection_engine/scripts/legacy_notifications/one_action.json` and add this id to the file. Something like this: + +```json +{ + "name": "Legacy notification with one action", + "interval": "1m", <--- You can use whatever you want. Real values are "1h", "1d", "1w". I use "1m" for testing purposes. + "actions": [ + { + "id": "879e8ff0-1be1-11ec-a722-83da1c22a481", <--- My action id + "group": "default", + "params": { + "message": "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" + }, + "actionTypeId": ".slack" <--- I am a slack action id type. + } + ] +} +``` + +Query for an alert you want to add manually add back a legacy notification to it. Such as: + +```json +# See all your siem.signals alert types and choose one +GET .kibana/_search +{ + "query": { + "term": { + "alert.alertTypeId": "siem.signals" + } + } +} +``` + +Grab the `_id` without the alert prefix. For mine this was `933ca720-1be1-11ec-a722-83da1c22a481` + +Within the directory of detection_engine/scripts execute the script: + +```json +./post_legacy_notification.sh 933ca720-1be1-11ec-a722-83da1c22a481 +{ + "ok": "acknowledged" +} +``` + +which is going to do a few things. See the file `detection_engine/routes/rules/legacy_create_legacy_notification.ts` for the definition of the route and what it does in full, but we should notice that we have now: + +Created a legacy side car action object of type `siem-detection-engine-rule-actions` you can see in dev tools: + +```json +# See the actions "side car" which are part of the legacy notification system. +GET .kibana/_search +{ + "query": { + "term": { + "type": { + "value": "siem-detection-engine-rule-actions" + } + } + } +} +``` + +But more importantly what the saved object references are which should be this: + +```json +# Get the alert type of "siem-notifications" which is part of the legacy system. +GET .kibana/_search +{ + "query": { + "term": { + "alert.alertTypeId": "siem.notifications" + } + } +} +``` + +I omit parts but leave the important parts pre-migration and post-migration of the Saved Object. + +```json +"data..omitted": "data..omitted", +"params" : { + "ruleAlertId" : "933ca720-1be1-11ec-a722-83da1c22a481" <-- Pre-migration we had this Saved Object ID which is not part of references array below +}, +"actions" : [ + { + "group" : "default", + "params" : { + "message" : "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" + }, + "actionTypeId" : ".slack", + "actionRef" : "action_0" <-- Pre-migration this is correct as this work is already done within the alerting plugin + }, + "references" : [ + { + "id" : "879e8ff0-1be1-11ec-a722-83da1c22a481", + "name" : "action_0", <-- Pre-migration this is correct as this work is already done within the alerting plugin + "type" : "action" + } + ] +], +"data..omitted": "data..omitted", +``` + +Post migration this structure should look like this after Kibana has started and finished the migration. + +```json +"data..omitted": "data..omitted", +"params" : { + "ruleAlertId" : "933ca720-1be1-11ec-a722-83da1c22a481" <-- Post-migration this is not used but rather the serialized version references is used instead. +}, +"actions" : [ + { + "group" : "default", + "params" : { + "message" : "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" + }, + "actionTypeId" : ".slack", + "actionRef" : "action_0" + }, + "references" : [ + { + "id" : "879e8ff0-1be1-11ec-a722-83da1c22a481", + "name" : "action_0", + "type" : "action" + }, + { + "id" : "933ca720-1be1-11ec-a722-83da1c22a481", <-- Our id here is preferred and used during serialization. + "name" : "param:alert_0", <-- We add the name of our reference which is param:alert_0 similar to action_0 but with "param" + "type" : "alert" <-- We add the type which is type of rule to the references + } + ] +], +"data..omitted": "data..omitted", +``` + +Only if for some reason a migration has failed due to a bug would we fallback and try to use `"ruleAlertId" : "933ca720-1be1-11ec-a722-83da1c22a481"`, as it was last stored within SavedObjects. Otherwise all access will come from the +references array's version. If the migration fails or the fallback to the last known saved object id `"ruleAlertId" : "933ca720-1be1-11ec-a722-83da1c22a481"` does happen, then the code emits several error messages to the +user which should further encourage the user to help migrate the legacy notification system to the newer notification system. + +## Useful queries + +This gives you back the legacy notifications. + +```json +# Get the alert type of "siem-notifications" which is part of the legacy system. +GET .kibana/_search +{ + "query": { + "term": { + "alert.alertTypeId": "siem.notifications" + } + } +} +``` + +If you need to ad-hoc test what happens when the migration runs you can get the id of an alert and downgrade it, then +restart Kibana. The `ctx._source.references.remove(1)` removes the last element of the references array which is assumed +to have a rule. But it might not, so ensure you check your data structure and adjust accordingly. +```json +POST .kibana/_update/alert:933ca720-1be1-11ec-a722-83da1c22a481 +{ + "script" : { + "source": """ + ctx._source.migrationVersion.alert = "7.15.0"; + ctx._source.references.remove(1); + """, + "lang": "painless" + } +} +``` + +If you just want to remove your "param:alert_0" and it is the second array element to test the errors within the console +then you would use +```json +POST .kibana/_update/alert:933ca720-1be1-11ec-a722-83da1c22a481 +{ + "script" : { + "source": """ + ctx._source.references.remove(1); + """, + "lang": "painless" + } +} +``` + +## End to end tests +See `test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts` for tests around migrations diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_references.test.ts new file mode 100644 index 000000000000..231451947a1d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_references.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesNotificationParams } from '../legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { legacyExtractReferences } from './legacy_extract_references'; + +describe('legacy_extract_references', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('It returns the references extracted as saved object references', () => { + const params: LegacyRulesNotificationParams = { + ruleAlertId: '123', + }; + expect( + legacyExtractReferences({ + logger, + params, + }) + ).toEqual({ + params, + references: [ + { + id: '123', + name: 'alert_0', + type: 'alert', + }, + ], + }); + }); + + test('It returns the empty references array if the ruleAlertId is missing for any particular unusual reason', () => { + const params = {}; + expect( + legacyExtractReferences({ + logger, + params: params as LegacyRulesNotificationParams, + }) + ).toEqual({ + params: params as LegacyRulesNotificationParams, + references: [], + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_references.ts new file mode 100644 index 000000000000..1461b78ba73a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_references.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from 'src/core/server'; +import { RuleParamsAndRefs } from '../../../../../../alerting/server'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesNotificationParams } from '../legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { legacyExtractRuleId } from './legacy_extract_rule_id'; + +/** + * Extracts references and returns the saved object references. + * NOTE: You should not have to add any new ones here at all, but this keeps consistency with the other + * version(s) used for security_solution rules. + * + * How to add a new extracted references here (This should be rare or non-existent): + * --- + * Add a new file for extraction named: extract_.ts, example: extract_foo.ts + * Add a function into that file named: extract, example: extractFoo(logger, params.foo) + * Add a new line below and concat together the new extract with existing ones like so: + * + * const legacyRuleIdReferences = legacyExtractRuleId(logger, params.ruleAlertId); + * const fooReferences = extractFoo(logger, params.foo); + * const returnReferences = [...legacyRuleIdReferences, ...fooReferences]; + * + * Optionally you can remove any parameters you do not want to store within the Saved Object here: + * const paramsWithoutSavedObjectReferences = { removeParam, ...otherParams }; + * + * If you do remove params, then update the types in: security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + * @param logger Kibana injected logger + * @param params The params of the base rule(s). + * @returns The rule parameters and the saved object references to store. + */ +export const legacyExtractReferences = ({ + logger, + params, +}: { + logger: Logger; + params: LegacyRulesNotificationParams; +}): RuleParamsAndRefs => { + const legacyRuleIdReferences = legacyExtractRuleId({ + logger, + ruleAlertId: params.ruleAlertId, + }); + const returnReferences = [...legacyRuleIdReferences]; + + // Modify params if you want to remove any elements separately here. For legacy ruleAlertId, we do not remove the id and instead + // keep it to both fail safe guard against manually removed saved object references or if there are migration issues and the saved object + // references are removed. Also keeping it we can detect and log out a warning if the reference between it and the saved_object reference + // array have changed between each other indicating the saved_object array is being mutated outside of this functionality + const paramsWithoutSavedObjectReferences = { ...params }; + + return { + references: returnReferences, + params: paramsWithoutSavedObjectReferences, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_rule_id.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_rule_id.test.ts new file mode 100644 index 000000000000..476a72461e8a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_rule_id.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesNotificationParams } from '../legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { legacyExtractRuleId } from './legacy_extract_rule_id'; + +describe('legacy_extract_rule_id', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('it returns an empty array given a "undefined" ruleAlertId.', () => { + expect( + legacyExtractRuleId({ + logger, + ruleAlertId: undefined as unknown as LegacyRulesNotificationParams['ruleAlertId'], + }) + ).toEqual([]); + }); + + test('logs expect error message if given a "undefined" ruleAlertId.', () => { + expect( + legacyExtractRuleId({ + logger, + ruleAlertId: null as unknown as LegacyRulesNotificationParams['ruleAlertId'], + }) + ).toEqual([]); + + expect(logger.error).toBeCalledWith( + 'Security Solution notification (Legacy) system "ruleAlertId" is null or undefined when it never should be. ,This indicates potentially that saved object migrations did not run correctly. Returning empty reference' + ); + }); + + test('it returns the "ruleAlertId" transformed into a saved object references array.', () => { + expect( + legacyExtractRuleId({ + logger, + ruleAlertId: '123', + }) + ).toEqual([ + { + id: '123', + name: 'alert_0', + type: 'alert', + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_rule_id.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_rule_id.ts new file mode 100644 index 000000000000..bc43fd59e68e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_extract_rule_id.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectReference } from 'src/core/server'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesNotificationParams } from '../legacy_types'; + +/** + * This extracts the "ruleAlertId" "id" and returns it as a saved object reference. + * NOTE: Due to rolling upgrades with migrations and a few bugs with migrations, I do an additional check for if "ruleAlertId" exists or not. Once + * those bugs are fixed, we can remove the "if (ruleAlertId == null) {" check, but for the time being it is there to keep things running even + * if ruleAlertId has not been migrated. + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + * @param logger The kibana injected logger + * @param ruleAlertId The rule alert id to get the id from and return it as a saved object reference. + * @returns The saved object references from the rule alert id + */ +export const legacyExtractRuleId = ({ + logger, + ruleAlertId, +}: { + logger: Logger; + ruleAlertId: LegacyRulesNotificationParams['ruleAlertId']; +}): SavedObjectReference[] => { + if (ruleAlertId == null) { + logger.error( + [ + 'Security Solution notification (Legacy) system "ruleAlertId" is null or undefined when it never should be. ', + 'This indicates potentially that saved object migrations did not run correctly. Returning empty reference', + ].join() + ); + return []; + } else { + return [ + { + id: ruleAlertId, + name: 'alert_0', + type: 'alert', + }, + ]; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_references.test.ts new file mode 100644 index 000000000000..ae34479e7353 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_references.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; + +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesNotificationParams } from '../legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { legacyInjectReferences } from './legacy_inject_references'; + +describe('legacy_inject_references', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: 'alert_0', + type: 'alert', + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('returns parameters from a saved object if found', () => { + const params: LegacyRulesNotificationParams = { + ruleAlertId: '123', + }; + + expect( + legacyInjectReferences({ + logger, + params, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(params); + }); + + test('returns parameters from the saved object if found with a different saved object reference id', () => { + const params: LegacyRulesNotificationParams = { + ruleAlertId: '123', + }; + + expect( + legacyInjectReferences({ + logger, + params, + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], + }) + ).toEqual({ + ruleAlertId: '456', + }); + }); + + test('It returns params with an added ruleAlertId if the ruleAlertId is missing due to migration bugs', () => { + const params = {} as LegacyRulesNotificationParams; + + expect( + legacyInjectReferences({ + logger, + params, + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], + }) + ).toEqual({ + ruleAlertId: '456', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_references.ts new file mode 100644 index 000000000000..5a7118d64ba3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_references.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectReference } from 'src/core/server'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesNotificationParams } from '../legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { legacyInjectRuleIdReferences } from './legacy_inject_rule_id_references'; + +/** + * Injects references and returns the saved object references. + * How to add a new injected references here (NOTE: We do not expect to add more here but we leave this as the same pattern we have in other reference sections): + * --- + * Add a new file for injection named: legacy_inject_.ts, example: legacy_inject_foo.ts + * Add a new function into that file named: legacy_inject, example: legacyInjectFooReferences(logger, params.foo) + * Add a new line below and spread the new parameter together like so: + * + * const foo = legacyInjectFooReferences(logger, params.foo, savedObjectReferences); + * const ruleParamsWithSavedObjectReferences: RuleParams = { + * ...params, + * foo, + * ruleAlertId, + * }; + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + * @param logger Kibana injected logger + * @param params The params of the base rule(s). + * @param savedObjectReferences The saved object references to merge with the rule params + * @returns The rule parameters with the saved object references. + */ +export const legacyInjectReferences = ({ + logger, + params, + savedObjectReferences, +}: { + logger: Logger; + params: LegacyRulesNotificationParams; + savedObjectReferences: SavedObjectReference[]; +}): LegacyRulesNotificationParams => { + const ruleAlertId = legacyInjectRuleIdReferences({ + logger, + ruleAlertId: params.ruleAlertId, + savedObjectReferences, + }); + const ruleParamsWithSavedObjectReferences: LegacyRulesNotificationParams = { + ...params, + ruleAlertId, + }; + return ruleParamsWithSavedObjectReferences; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.test.ts new file mode 100644 index 000000000000..2f63a184875f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; + +// eslint-disable-next-line no-restricted-imports +import { legacyInjectRuleIdReferences } from './legacy_inject_rule_id_references'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesNotificationParams } from '../legacy_types'; + +describe('legacy_inject_rule_id_references', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: 'alert_0', + type: 'alert', + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('returns parameters from the saved object if found', () => { + expect( + legacyInjectRuleIdReferences({ + logger, + ruleAlertId: '123', + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual('123'); + }); + + test('returns parameters from the saved object if "ruleAlertId" is undefined', () => { + expect( + legacyInjectRuleIdReferences({ + logger, + ruleAlertId: undefined as unknown as LegacyRulesNotificationParams['ruleAlertId'], + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual('123'); + }); + + test('prefers to use saved object references if the two are different from each other', () => { + expect( + legacyInjectRuleIdReferences({ + logger, + ruleAlertId: '456', + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual('123'); + }); + + test('returns sent in "ruleAlertId" if the saved object references is empty', () => { + expect( + legacyInjectRuleIdReferences({ + logger, + ruleAlertId: '456', + savedObjectReferences: [], + }) + ).toEqual('456'); + }); + + test('does not log an error if it returns parameters from the saved object when found', () => { + legacyInjectRuleIdReferences({ + logger, + ruleAlertId: '123', + savedObjectReferences: mockSavedObjectReferences(), + }); + expect(logger.error).not.toHaveBeenCalled(); + }); + + test('logs an error if found with a different saved object reference id', () => { + legacyInjectRuleIdReferences({ + logger, + ruleAlertId: '456', + savedObjectReferences: mockSavedObjectReferences(), + }); + expect(logger.error).toBeCalledWith( + 'The id of the "saved object reference id": 123 is not the same as the "saved object id": 456. Preferring and using the "saved object reference id" instead of the "saved object id"' + ); + }); + + test('logs an error if the saved object references is empty', () => { + legacyInjectRuleIdReferences({ + logger, + ruleAlertId: '123', + savedObjectReferences: [], + }); + expect(logger.error).toBeCalledWith( + 'The saved object reference was not found for the "ruleAlertId" when we were expecting to find it. Kibana migrations might not have run correctly or someone might have removed the saved object references manually. Returning the last known good "ruleAlertId" which might not work. "ruleAlertId" with its id being returned is: 123' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.ts new file mode 100644 index 000000000000..5cb32c656315 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectReference } from 'src/core/server'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesNotificationParams } from '../legacy_types'; + +/** + * This injects any legacy "id"'s from saved object reference and returns the "ruleAlertId" using the saved object reference. If for + * some reason it is missing on saved object reference, we log an error about it and then take the last known good value from the "ruleId" + * + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + * @param logger The kibana injected logger + * @param ruleAlertId The alert id to merge the saved object reference from. + * @param savedObjectReferences The saved object references which should contain a "ruleAlertId" + * @returns The "ruleAlertId" with the saved object reference replacing any value in the saved object's id. + */ +export const legacyInjectRuleIdReferences = ({ + logger, + ruleAlertId, + savedObjectReferences, +}: { + logger: Logger; + ruleAlertId: LegacyRulesNotificationParams['ruleAlertId']; + savedObjectReferences: SavedObjectReference[]; +}): LegacyRulesNotificationParams['ruleAlertId'] => { + const referenceFound = savedObjectReferences.find((reference) => { + return reference.name === 'alert_0'; + }); + if (referenceFound) { + if (referenceFound.id !== ruleAlertId) { + // This condition should not be reached but we log an error if we encounter it to help if we migrations + // did not run correctly or we create a regression in the future. + logger.error( + [ + 'The id of the "saved object reference id": ', + referenceFound.id, + ' is not the same as the "saved object id": ', + ruleAlertId, + '. Preferring and using the "saved object reference id" instead of the "saved object id"', + ].join('') + ); + } + return referenceFound.id; + } else { + logger.error( + [ + 'The saved object reference was not found for the "ruleAlertId" when we were expecting to find it. ', + 'Kibana migrations might not have run correctly or someone might have removed the saved object references manually. ', + 'Returning the last known good "ruleAlertId" which might not work. "ruleAlertId" with its id being returned is: ', + ruleAlertId, + ].join('') + ); + return ruleAlertId; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_types.ts index 2a52f1437984..28fa62f28ed2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { schema, TypeOf } from '@kbn/config-schema'; + import { RulesClient, PartialAlert, @@ -102,8 +104,8 @@ export type LegacyNotificationExecutorOptions = AlertExecutorOptions< export const legacyIsNotificationAlertExecutor = ( obj: LegacyNotificationAlertTypeDefinition ): obj is AlertType< - AlertTypeParams, - AlertTypeParams, + LegacyRuleNotificationAlertTypeParams, + LegacyRuleNotificationAlertTypeParams, AlertTypeState, AlertInstanceState, AlertInstanceContext @@ -116,8 +118,8 @@ export const legacyIsNotificationAlertExecutor = ( */ export type LegacyNotificationAlertTypeDefinition = Omit< AlertType< - AlertTypeParams, - AlertTypeParams, + LegacyRuleNotificationAlertTypeParams, + LegacyRuleNotificationAlertTypeParams, AlertTypeState, AlertInstanceState, AlertInstanceContext, @@ -131,3 +133,19 @@ export type LegacyNotificationAlertTypeDefinition = Omit< state, }: LegacyNotificationExecutorOptions) => Promise; }; + +/** + * This is the notification type used within legacy_rules_notification_alert_type for the alert params. + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + * @see legacy_rules_notification_alert_type + */ +export const legacyRulesNotificationParams = schema.object({ + ruleAlertId: schema.string(), +}); + +/** + * This legacy rules notification type used within legacy_rules_notification_alert_type for the alert params. + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + * @see legacy_rules_notification_alert_type + */ +export type LegacyRulesNotificationParams = TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index b47a9fc3a5d6..6039ad6ab612 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -35,8 +35,8 @@ type SecuritySolutionRequestHandlerContextMock = SecuritySolutionRequestHandlerC asCurrentUser: { updateByQuery: jest.Mock; search: jest.Mock; - transport: { - request: jest.Mock; + security: { + hasPrivileges: jest.Mock; }; }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index b79bdc857a17..7ffa45e2bf7e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -19,7 +19,7 @@ describe('read_privileges route', () => { server = serverMock.create(); ({ context } = requestContextMock.createTools()); - context.core.elasticsearch.client.asCurrentUser.transport.request.mockResolvedValue({ + context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges.mockResolvedValue({ body: getMockPrivilegesResult(), }); @@ -65,7 +65,7 @@ describe('read_privileges route', () => { }); test('returns 500 when bad response from cluster', async () => { - context.core.elasticsearch.client.asCurrentUser.transport.request.mockResolvedValue( + context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges.mockResolvedValue( elasticsearchClientMock.createErrorTransportRequestPromise(new Error('Test error')) ); const response = await server.inject( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json index b1500ac6fa6b..1966dcf5ff53 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json @@ -3,7 +3,7 @@ "interval": "1m", "actions": [ { - "id": "879e8ff0-1be1-11ec-a722-83da1c22a481", + "id": "42534430-2092-11ec-99a6-05d79563c01a", "group": "default", "params": { "message": "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md index 893797afa44d..c76a69db084c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md @@ -21,6 +21,26 @@ GET .kibana/_search } ``` +If you want to manually test the downgrade of an alert then you can use this script. +```json +# Set saved object array references as empty arrays and set our migration version to be 7.14.0 +POST .kibana/_update/alert:38482620-ef1b-11eb-ad71-7de7959be71c +{ + "script" : { + "source": """ + ctx._source.migrationVersion.alert = "7.14.0"; + ctx._source.references = [] + """, + "lang": "painless" + } +} +``` + +Reload the alert in the security_solution and notice you get these errors until you restart Kibana to cause a migration moving forward. Although you get errors, +everything should still operate normally as we try to work even if migrations did not run correctly for any unforeseen reasons. + +For testing idempotentence, just re-run the same script above for a downgrade after you restarted Kibana. + ## Structure on disk Run a query in dev tools and you should see this code that adds the following savedObject references to any newly saved rule: @@ -141,4 +161,4 @@ Good examples and utilities can be found in the folder of `utils` such as: You can follow those patterns but if it doesn't fit your use case it's fine to just create a new file and wire up your new saved object references ## End to end tests -At this moment there are none. \ No newline at end of file +See `test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts` for tests around migrations diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index 8b715b8e8d58..038b7687784f 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -84,7 +84,7 @@ export class TelemetryReceiver { policy_responses: { terms: { size: this.max_records, - field: 'Endpoint.policy.applied.id', + field: 'agent.id', }, aggs: { latest_response: { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts index 2cebbc0af69c..0c066deea17d 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts @@ -190,13 +190,11 @@ export class TelemetryEndpointTask { * * As the policy id + policy version does not exist on the Endpoint Metrics document * we need to fetch information about the Fleet Agent and sync the metrics document - * with the Fleet agent's policy data. + * with the Agent's policy data. * - * 7.14 ~ An issue was created with the Endpoint agent team to add the policy id + - * policy version to the metrics document to circumvent and refactor away from - * this expensive join operation. */ const agentsResponse = endpointData.fleetAgentsResponse; + if (agentsResponse === undefined) { this.logger.debug('no fleet agent information available'); return 0; @@ -286,7 +284,7 @@ export class TelemetryEndpointTask { policyConfig = endpointPolicyCache.get(policyInformation) || null; if (policyConfig) { - failedPolicy = policyResponses.get(policyConfig?.id); + failedPolicy = policyResponses.get(endpointAgentId); } } @@ -294,7 +292,6 @@ export class TelemetryEndpointTask { return { '@timestamp': executeTo, - agent_id: fleetAgentId, endpoint_id: endpointAgentId, endpoint_version: endpoint.endpoint_version, endpoint_package_version: policyConfig?.package?.version || null, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/__mocks__/resolve_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/__mocks__/resolve_timeline.ts new file mode 100644 index 000000000000..a6508f609d9d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/__mocks__/resolve_timeline.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TimelineStatus, TimelineType } from '../../../../common/types/timeline'; +import { ResolvedTimelineWithOutcomeSavedObject } from './../../../../common/types/timeline/index'; + +export const mockResolvedSavedObject = { + saved_object: { + id: '760d3d20-2142-11ec-a46f-051cb8e3154c', + type: 'siem-ui-timeline', + namespaces: ['default'], + updated_at: '2021-09-29T16:29:48.478Z', + version: 'WzYxNzc0LDFd', + attributes: { + columns: [], + dataProviders: [], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + timelineType: 'default', + kqlQuery: { + filterQuery: null, + }, + title: 'Test Timeline', + sort: [ + { + columnType: 'date', + sortDirection: 'desc', + columnId: '@timestamp', + }, + ], + status: 'active', + created: 1632932987378, + createdBy: 'tester', + updated: 1632932988422, + updatedBy: 'tester', + templateTimelineId: null, + templateTimelineVersion: null, + excludedRowRendererIds: [], + dateRange: { + start: '2021-09-29T04:00:00.000Z', + end: '2021-09-30T03:59:59.999Z', + }, + indexNames: [], + eqlOptions: { + tiebreakerField: '', + size: 100, + query: '', + eventCategoryField: 'event.category', + timestampField: '@timestamp', + }, + }, + references: [], + migrationVersion: { + 'siem-ui-timeline': '7.16.0', + }, + coreMigrationVersion: '8.0.0', + }, + outcome: 'aliasMatch', + alias_target_id: 'new-saved-object-id', +}; + +export const mockResolvedTimeline = { + savedObjectId: '760d3d20-2142-11ec-a46f-051cb8e3154c', + version: 'WzY1NDcxLDFd', + columns: [], + dataProviders: [], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + timelineType: TimelineType.default, + kqlQuery: { filterQuery: null }, + title: 'Test Timeline', + sort: [ + { + columnType: 'date', + sortDirection: 'desc', + columnId: '@timestamp', + }, + ], + status: TimelineStatus.active, + created: 1632932987378, + createdBy: 'tester', + updated: 1632932988422, + updatedBy: 'tester', + templateTimelineId: null, + templateTimelineVersion: null, + excludedRowRendererIds: [], + dateRange: { + start: '2021-09-29T04:00:00.000Z', + end: '2021-09-30T03:59:59.999Z', + }, + indexNames: [], + eqlOptions: { + tiebreakerField: '', + size: 100, + query: '', + eventCategoryField: 'event.category', + timestampField: '@timestamp', + }, + savedQueryId: null, +}; + +export const mockPopulatedTimeline = { + ...mockResolvedTimeline, + eventIdToNoteIds: [], + favorite: [], + noteIds: [], + notes: [], + pinnedEventIds: [], + pinnedEventsSaveObject: [], +}; + +export const mockResolveTimelineResponse: ResolvedTimelineWithOutcomeSavedObject = { + timeline: mockPopulatedTimeline, + outcome: 'aliasMatch', + alias_target_id: 'new-saved-object-id', +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/index.ts index ebd0dbba7d19..ba20633a6514 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/index.ts @@ -12,3 +12,4 @@ export { getTimelinesRoute } from './get_timelines'; export { importTimelinesRoute } from './import_timelines'; export { patchTimelinesRoute } from './patch_timelines'; export { persistFavoriteRoute } from './persist_favorite'; +export { resolveTimelineRoute } from './resolve_timeline'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts new file mode 100644 index 000000000000..04aa6fef3a37 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { transformError } from '@kbn/securitysolution-es-utils'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; + +import { TIMELINE_RESOLVE_URL } from '../../../../../../common/constants'; + +import { ConfigType } from '../../../../..'; +import { SetupPlugins } from '../../../../../plugin'; +import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; + +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; + +import { buildFrameworkRequest } from '../../../utils/common'; +import { getTimelineQuerySchema } from '../../../schemas/timelines'; +import { getTimelineTemplateOrNull, resolveTimelineOrNull } from '../../../saved_object/timelines'; + +export const resolveTimelineRoute = ( + router: SecuritySolutionPluginRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { + router.get( + { + path: TIMELINE_RESOLVE_URL, + validate: { + query: buildRouteValidationWithExcess(getTimelineQuerySchema), + }, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + try { + const frameworkRequest = await buildFrameworkRequest(context, security, request); + const query = request.query ?? {}; + const { template_timeline_id: templateTimelineId, id } = query; + + let res = null; + + if (templateTimelineId != null && id == null) { + // Template timelineId is not a SO id, so it does not need to be updated to use resolve + res = await getTimelineTemplateOrNull(frameworkRequest, templateTimelineId); + } else if (templateTimelineId == null && id != null) { + // In the event the objectId is defined, run the resolve call + res = await resolveTimelineOrNull(frameworkRequest, id); + } else { + throw new Error('please provide id or template_timeline_id'); + } + + return response.ok({ body: res ? { data: res } : {} }); + } catch (err) { + const error = transformError(err); + const siemResponse = buildSiemResponse(response); + + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.test.ts index f7e4de69097f..112796df527f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.test.ts @@ -8,11 +8,24 @@ import { FrameworkRequest } from '../../../framework'; import { mockGetTimelineValue, mockSavedObject } from '../../__mocks__/import_timelines'; -import { convertStringToBase64, getExistingPrepackagedTimelines, getAllTimeline } from '.'; +import { + convertStringToBase64, + getExistingPrepackagedTimelines, + getAllTimeline, + resolveTimelineOrNull, +} from '.'; import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; import { getNotesByTimelineId } from '../notes/saved_object'; import { getAllPinnedEventsByTimelineId } from '../pinned_events'; -import { AllTimelinesResponse } from '../../../../../common/types/timeline'; +import { + AllTimelinesResponse, + ResolvedTimelineWithOutcomeSavedObject, +} from '../../../../../common/types/timeline'; +import { + mockResolvedSavedObject, + mockResolvedTimeline, + mockResolveTimelineResponse, +} from '../../__mocks__/resolve_timeline'; jest.mock('./convert_saved_object_to_savedtimeline', () => ({ convertSavedObjectToSavedTimeline: jest.fn(), @@ -151,7 +164,7 @@ describe('saved_object', () => { (getAllPinnedEventsByTimelineId as jest.Mock).mockClear(); }); - test('should send correct options if no filters applys', async () => { + test('should send correct options if no filters applies', async () => { expect(mockFindSavedObject.mock.calls[0][0]).toEqual({ filter: 'not siem-ui-timeline.attributes.status: draft', page: pageInfo.pageIndex, @@ -226,7 +239,7 @@ describe('saved_object', () => { ); }); - test('should retuen correct result', async () => { + test('should return correct result', async () => { expect(result).toEqual({ totalCount: 1, customTemplateTimelineCount: 0, @@ -248,4 +261,52 @@ describe('saved_object', () => { }); }); }); + + describe('resolveTimelineOrNull', () => { + let mockResolveSavedObject: jest.Mock; + let mockRequest: FrameworkRequest; + let result: ResolvedTimelineWithOutcomeSavedObject | null = null; + beforeEach(async () => { + (convertSavedObjectToSavedTimeline as jest.Mock).mockReturnValue(mockResolvedTimeline); + mockResolveSavedObject = jest.fn().mockReturnValue(mockResolvedSavedObject); + mockRequest = { + user: { + username: 'username', + }, + context: { + core: { + savedObjects: { + client: { + resolve: mockResolveSavedObject, + }, + }, + }, + }, + } as unknown as FrameworkRequest; + + result = await resolveTimelineOrNull(mockRequest, '760d3d20-2142-11ec-a46f-051cb8e3154c'); + }); + + afterEach(() => { + mockResolveSavedObject.mockClear(); + (getNotesByTimelineId as jest.Mock).mockClear(); + (getAllPinnedEventsByTimelineId as jest.Mock).mockClear(); + }); + + test('should call getNotesByTimelineId', async () => { + expect((getNotesByTimelineId as jest.Mock).mock.calls[0][1]).toEqual( + mockResolvedSavedObject.saved_object.id + ); + }); + + test('should call getAllPinnedEventsByTimelineId', async () => { + expect((getAllPinnedEventsByTimelineId as jest.Mock).mock.calls[0][1]).toEqual( + mockResolvedSavedObject.saved_object.id + ); + }); + + test('should return the timeline with resolve attributes', async () => { + expect(result).toEqual(mockResolveTimelineResponse); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts index b3ed5675cea3..cc28e0c9eb85 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts @@ -30,6 +30,7 @@ import { TimelineStatus, TimelineResult, TimelineWithoutExternalRefs, + ResolvedTimelineWithOutcomeSavedObject, } from '../../../../../common/types/timeline'; import { FrameworkRequest } from '../../../framework'; import * as note from '../notes/saved_object'; @@ -52,49 +53,6 @@ export interface ResponseTemplateTimeline { templateTimeline: TimelineResult; } -export interface Timeline { - getTimeline: ( - request: FrameworkRequest, - timelineId: string, - timelineType?: TimelineTypeLiteralWithNull - ) => Promise; - - getAllTimeline: ( - request: FrameworkRequest, - onlyUserFavorite: boolean | null, - pageInfo: PageInfoTimeline, - search: string | null, - sort: SortTimeline | null, - status: TimelineStatusLiteralWithNull, - timelineType: TimelineTypeLiteralWithNull - ) => Promise; - - persistFavorite: ( - request: FrameworkRequest, - timelineId: string | null, - templateTimelineId: string | null, - templateTimelineVersion: number | null, - timelineType: TimelineType - ) => Promise; - - persistTimeline: ( - request: FrameworkRequest, - timelineId: string | null, - version: string | null, - timeline: SavedTimeline, - isImmutable?: boolean - ) => Promise; - - deleteTimeline: (request: FrameworkRequest, timelineIds: string[]) => Promise; - convertStringToBase64: (text: string) => string; - timelineWithReduxProperties: ( - notes: NoteSavedObject[], - pinnedEvents: PinnedEventSavedObject[], - timeline: TimelineSavedObject, - userName: string - ) => TimelineSavedObject; -} - export const getTimeline = async ( request: FrameworkRequest, timelineId: string, @@ -132,6 +90,18 @@ export const getTimelineOrNull = async ( return timeline; }; +export const resolveTimelineOrNull = async ( + frameworkRequest: FrameworkRequest, + savedObjectId: string +): Promise => { + let resolvedTimeline = null; + try { + resolvedTimeline = await resolveSavedTimeline(frameworkRequest, savedObjectId); + // eslint-disable-next-line no-empty + } catch (e) {} + return resolvedTimeline; +}; + export const getTimelineByTemplateTimelineId = async ( request: FrameworkRequest, templateTimelineId: string @@ -584,6 +554,44 @@ export const deleteTimeline = async (request: FrameworkRequest, timelineIds: str ); }; +const resolveBasicSavedTimeline = async (request: FrameworkRequest, timelineId: string) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const { saved_object: savedObject, ...resolveAttributes } = + await savedObjectsClient.resolve( + timelineSavedObjectType, + timelineId + ); + + const populatedTimeline = timelineFieldsMigrator.populateFieldsFromReferences(savedObject); + + return { + resolvedTimelineSavedObject: convertSavedObjectToSavedTimeline(populatedTimeline), + ...resolveAttributes, + }; +}; + +const resolveSavedTimeline = async (request: FrameworkRequest, timelineId: string) => { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; + + const { resolvedTimelineSavedObject, ...resolveAttributes } = await resolveBasicSavedTimeline( + request, + timelineId + ); + + const timelineWithNotesAndPinnedEvents = await Promise.all([ + note.getNotesByTimelineId(request, resolvedTimelineSavedObject.savedObjectId), + pinnedEvent.getAllPinnedEventsByTimelineId(request, resolvedTimelineSavedObject.savedObjectId), + resolvedTimelineSavedObject, + ]); + + const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents; + + return { + timeline: timelineWithReduxProperties(notes, pinnedEvents, timeline, userName), + ...resolveAttributes, + }; +}; + const getBasicSavedTimeline = async (request: FrameworkRequest, timelineId: string) => { const savedObjectsClient = request.context.core.savedObjects.client; const savedObject = await savedObjectsClient.get( @@ -646,13 +654,6 @@ const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObje 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[], diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 391beb3c4012..f69565cacceb 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -59,6 +59,7 @@ import { initRoutes } from './routes'; import { isAlertExecutor } from './lib/detection_engine/signals/types'; import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; import { ManifestTask } from './endpoint/lib/artifacts'; +import { CheckMetadataTransformsTask } from './endpoint/lib/metadata'; import { initSavedObjects } from './saved_objects'; import { AppClientFactory } from './client'; import { createConfig, ConfigType } from './config'; @@ -157,6 +158,7 @@ export class Plugin implements IPlugin; private telemetryUsageCounter?: UsageCounter; @@ -363,6 +365,12 @@ export class Plugin implements IPlugin { doc_count: 13, }, ownerIds: { - value: 1, + ownerIds: { + value: 1, + }, }, // The `FiltersAggregate` doesn't cover the case of a nested `AggregationsAggregationContainer`, in which `FiltersAggregate` // would not have a `buckets` property, but rather a keyed property that's inferred from the request. @@ -127,8 +129,19 @@ describe('Workload Statistics Aggregator', () => { missing: { field: 'task.schedule' }, }, ownerIds: { - cardinality: { - field: 'task.ownerId', + filter: { + range: { + 'task.startedAt': { + gte: 'now-1w/w', + }, + }, + }, + aggs: { + ownerIds: { + cardinality: { + field: 'task.ownerId', + }, + }, }, }, idleTasks: { @@ -264,7 +277,9 @@ describe('Workload Statistics Aggregator', () => { doc_count: 13, }, ownerIds: { - value: 1, + ownerIds: { + value: 1, + }, }, // The `FiltersAggregate` doesn't cover the case of a nested `AggregationsAggregationContainer`, in which `FiltersAggregate` // would not have a `buckets` property, but rather a keyed property that's inferred from the request. @@ -605,7 +620,9 @@ describe('Workload Statistics Aggregator', () => { doc_count: 13, }, ownerIds: { - value: 3, + ownerIds: { + value: 3, + }, }, // The `FiltersAggregate` doesn't cover the case of a nested `AggregationContainer`, in which `FiltersAggregate` // would not have a `buckets` property, but rather a keyed property that's inferred from the request. diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index b833e4ed5753..9ac528cfd1ce 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -147,8 +147,19 @@ export function createWorkloadAggregator( missing: { field: 'task.schedule' }, }, ownerIds: { - cardinality: { - field: 'task.ownerId', + filter: { + range: { + 'task.startedAt': { + gte: 'now-1w/w', + }, + }, + }, + aggs: { + ownerIds: { + cardinality: { + field: 'task.ownerId', + }, + }, }, }, idleTasks: { @@ -213,7 +224,7 @@ export function createWorkloadAggregator( const taskTypes = aggregations.taskType.buckets; const nonRecurring = aggregations.nonRecurringTasks.doc_count; - const ownerIds = aggregations.ownerIds.value; + const ownerIds = aggregations.ownerIds.ownerIds.value; const { overdue: { @@ -448,7 +459,9 @@ export interface WorkloadAggregationResponse { doc_count: number; }; ownerIds: { - value: number; + ownerIds: { + value: number; + }; }; [otherAggs: string]: estypes.AggregationsAggregate; } diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx index 5fe766077a74..02c3d10a7605 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx @@ -228,14 +228,13 @@ export const getCombinedFilterQuery = ({ to, filters, ...combineQueriesParams -}: CombineQueries & { from: string; to: string }): string => { - return replaceStatusField( +}: CombineQueries & { from: string; to: string }): string => + replaceStatusField( combineQueries({ ...combineQueriesParams, filters: [...filters, buildTimeRangeFilter(from, to)], - })!.filterQuery + })?.filterQuery ); -}; /** * This function is a temporary patch to prevent queries using old `signal.status` field. @@ -243,8 +242,8 @@ export const getCombinedFilterQuery = ({ * must be replaced by `ALERT_WORKFLOW_STATUS` field name constant * @deprecated */ -const replaceStatusField = (query: string): string => - query.replaceAll('signal.status', ALERT_WORKFLOW_STATUS); +const replaceStatusField = (filterQuery?: string): string => + filterQuery?.replaceAll('signal.status', ALERT_WORKFLOW_STATUS) ?? ''; /** * The CSS class name of a "stateful event", which appears in both diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c2d46fa5762d..26f021379a2a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1653,12 +1653,12 @@ "data.functions.esaggs.help": "AggConfig 集約を実行します", "data.functions.esaggs.inspector.dataRequest.description": "このリクエストはElasticsearchにクエリし、ビジュアライゼーション用のデータを取得します。", "data.functions.esaggs.inspector.dataRequest.title": "データ", - "data.functions.indexPatternLoad.help": "インデックスパターンを読み込みます", - "data.functions.indexPatternLoad.id.help": "読み込むインデックスパターンID", - "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", - "data.indexPatterns.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title}(ID:{id})", - "data.indexPatterns.indexPatternLoad.error.kibanaRequest": "サーバーでこの検索を実行するには、KibanaRequest が必要です。式実行パラメーターに要求オブジェクトを渡してください。", - "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません。このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", + "dataViews.indexPatternLoad.help": "インデックスパターンを読み込みます", + "dataViews.functions.indexPatternLoad.id.help": "読み込むインデックスパターンID", + "dataViews.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", + "dataViews.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title}(ID:{id})", + "dataViews.indexPatternLoad.error.kibanaRequest": "サーバーでこの検索を実行するには、KibanaRequest が必要です。式実行パラメーターに要求オブジェクトを渡してください。", + "dataViews.unableWriteLabel": "インデックスパターンを書き込めません。このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", "data.inspector.table..dataDescriptionTooltip": "ビジュアライゼーションの元のデータを表示", "data.inspector.table.dataTitle": "データ", "data.inspector.table.downloadCSVToggleButtonLabel": "CSV をダウンロード", @@ -5198,12 +5198,6 @@ "visTypeTimeseries.indexPattern.timeRange.lastValue": "最終値", "visTypeTimeseries.indexPattern.timeRange.selectTimeRange": "選択してください", "visTypeTimeseries.indexPattern.сoarse": "粗い", - "visTypeTimeseries.indexPatternSelect.createIndexPatternText": "インデックスパターンを作成", - "visTypeTimeseries.indexPatternSelect.defaultIndexPatternText": "デフォルトのインデックスパターンが使用されています。", - "visTypeTimeseries.indexPatternSelect.label": "インデックスパターン", - "visTypeTimeseries.indexPatternSelect.queryAllIndexesText": "すべてのインデックスにクエリを実行するには * を使用します", - "visTypeTimeseries.indexPatternSelect.switchModePopover.areaLabel": "インデックスパターン選択モードを構成", - "visTypeTimeseries.indexPatternSelect.switchModePopover.title": "インデックスパターン選択モード", "visTypeTimeseries.kbnVisTypes.metricsDescription": "時系列データの高度な分析を実行します。", "visTypeTimeseries.kbnVisTypes.metricsTitle": "TSVB", "visTypeTimeseries.lastValueModeIndicator.lastBucketDate": "バケット:{lastBucketDate}", @@ -5332,7 +5326,6 @@ "visTypeTimeseries.seriesConfig.ignoreGlobalFilterLabel": "グローバルフィルターを無視しますか?", "visTypeTimeseries.seriesConfig.missingSeriesComponentDescription": "パネルタイプ {panelType} の数列コンポーネントが欠けています", "visTypeTimeseries.seriesConfig.offsetSeriesTimeLabel": "数列の時間を(1m, 1h, 1w, 1d)でオフセット", - "visTypeTimeseries.seriesConfig.overrideIndexPatternLabel": "インデックスパターンを上書きしますか?", "visTypeTimeseries.seriesConfig.templateHelpText": "eg. {templateExample}", "visTypeTimeseries.seriesConfig.templateLabel": "テンプレート", "visTypeTimeseries.sort.dragToSortAriaLabel": "ドラッグして並べ替えます", @@ -5463,7 +5456,6 @@ "visTypeTimeseries.timeseries.optionsTab.styleLabel": "スタイル", "visTypeTimeseries.timeseries.optionsTab.tooltipMode": "ツールチップ", "visTypeTimeseries.timeseries.optionsTab.truncateLegendLabel": "凡例を切り捨てますか?", - "visTypeTimeseries.timeSeries.overrideIndexPatternLabel": "インデックスパターンを上書きしますか?", "visTypeTimeseries.timeSeries.percentLabel": "パーセント", "visTypeTimeseries.timeseries.positionOptions.leftLabel": "左", "visTypeTimeseries.timeseries.positionOptions.rightLabel": "右", @@ -5528,9 +5520,8 @@ "visTypeTimeseries.visEditorVisualization.changesHaveNotBeenAppliedMessage": "ビジュアライゼーションへの変更が適用されました。", "visTypeTimeseries.visEditorVisualization.changesSuccessfullyAppliedMessage": "最新の変更が適用されました。", "visTypeTimeseries.visEditorVisualization.changesWillBeAutomaticallyAppliedMessage": "変更が自動的に適用されます。", - "visTypeTimeseries.visEditorVisualization.indexPatternMode.dismissNoticeButtonText": "閉じる", - "visTypeTimeseries.visEditorVisualization.indexPatternMode.link": "確認してください。", - "visTypeTimeseries.visEditorVisualization.indexPatternMode.notificationTitle": "TSVBはインデックスパターンをサポートします", + "visTypeTimeseries.visEditorVisualization.dataViewMode.dismissNoticeButtonText": "閉じる", + "visTypeTimeseries.visEditorVisualization.dataViewMode.link": "確認してください。", "visTypeTimeseries.visPicker.gaugeLabel": "ゲージ", "visTypeTimeseries.visPicker.metricLabel": "メトリック", "visTypeTimeseries.visPicker.tableLabel": "表", @@ -6319,9 +6310,6 @@ "xpack.apm.exactTransactionRateLabel": "{value} { unit, select, minute {tpm} other {tps} }", "xpack.apm.failedTransactionsCorrelations.licenseCheckText": "失敗したトランザクションの相関関係機能を使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。", "xpack.apm.featureRegistry.apmFeatureName": "APMおよびユーザーエクスペリエンス", - "xpack.apm.featureRegistry.manageAlertsName": "アラート", - "xpack.apm.featureRegistry.subfeature.alertsAllName": "すべて", - "xpack.apm.featureRegistry.subfeature.alertsReadName": "読み取り", "xpack.apm.feedbackMenu.appName": "APM", "xpack.apm.fetcher.error.status": "エラー", "xpack.apm.fetcher.error.title": "リソースの取得中にエラーが発生しました", @@ -6441,24 +6429,6 @@ "xpack.apm.localFilters.titles.serviceName": "サービス名", "xpack.apm.localFilters.titles.transactionUrl": "URL", "xpack.apm.localFiltersTitle": "フィルター", - "xpack.apm.metadataTable.section.agentLabel": "エージェント", - "xpack.apm.metadataTable.section.clientLabel": "クライアント", - "xpack.apm.metadataTable.section.containerLabel": "コンテナー", - "xpack.apm.metadataTable.section.customLabel": "カスタム", - "xpack.apm.metadataTable.section.errorLabel": "エラー", - "xpack.apm.metadataTable.section.hostLabel": "ホスト", - "xpack.apm.metadataTable.section.httpLabel": "HTTP", - "xpack.apm.metadataTable.section.labelsLabel": "ラベル", - "xpack.apm.metadataTable.section.messageLabel": "メッセージ", - "xpack.apm.metadataTable.section.pageLabel": "ページ", - "xpack.apm.metadataTable.section.processLabel": "プロセス", - "xpack.apm.metadataTable.section.serviceLabel": "サービス", - "xpack.apm.metadataTable.section.spanLabel": "スパン", - "xpack.apm.metadataTable.section.traceLabel": "トレース", - "xpack.apm.metadataTable.section.transactionLabel": "トランザクション", - "xpack.apm.metadataTable.section.urlLabel": "URL", - "xpack.apm.metadataTable.section.userAgentLabel": "ユーザーエージェント", - "xpack.apm.metadataTable.section.userLabel": "ユーザー", "xpack.apm.metrics.transactionChart.machineLearningLabel": "機械学習:", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "ストリームには、平均レイテンシの想定境界が表示されます。赤色の垂直の注釈は、異常スコアが75以上の異常値を示します。", "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "フィルタリングで検索バーを使用しているときには、機械学習結果が表示されません", @@ -6771,10 +6741,6 @@ "xpack.apm.settings.schema.confirm.title": "選択内容を確認してください", "xpack.apm.settings.schema.confirm.unsupportedConfigs.descriptionText": "互換性のあるカスタムapm-server.ymlユーザー設定がFleetサーバー設定に移動されます。削除する前に互換性のない設定について通知されます。", "xpack.apm.settings.schema.confirm.unsupportedConfigs.title": "次のapm-server.ymlユーザー設定は互換性がないため削除されます", - "xpack.apm.settings.schema.descriptionText": "クラシックAPMインデックスから切り替え、新しいデータストリーム機能をすぐに活用するためのシンプルでシームレスなプロセスを構築しました。このアクションは{irreversibleEmphasis}。また、Fleetへのアクセス権が付与された{superuserEmphasis}のみが実行できます。{dataStreamsDocLink}の詳細を参照してください。", - "xpack.apm.settings.schema.descriptionText.betaCalloutMessage": "この機能はベータ段階で、変更される可能性があります。デザインとコードはオフィシャルGA機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", - "xpack.apm.settings.schema.descriptionText.betaCalloutTitle": "APMのデータストリームはベータです", - "xpack.apm.settings.schema.descriptionText.dataStreamsDocLinkText": "データストリーム", "xpack.apm.settings.schema.descriptionText.irreversibleEmphasisText": "元に戻せません", "xpack.apm.settings.schema.descriptionText.superuserEmphasisText": "スーパーユーザー", "xpack.apm.settings.schema.disabledReason": "データストリームへの切り替えを使用できません: {reasons}", @@ -6784,9 +6750,6 @@ "xpack.apm.settings.schema.migrate.classicIndices.currentSetup": "現在の設定", "xpack.apm.settings.schema.migrate.classicIndices.description": "現在、データのクラシックAPMインデックスを使用しています。このデータスキーマは廃止予定であり、Elastic Stackバージョン8.0でデータストリームに置換されます。", "xpack.apm.settings.schema.migrate.classicIndices.title": "クラシックAPMインデックス", - "xpack.apm.settings.schema.migrate.dataStreams.betaBadge.description": "データストリームへの切り替えはGAではありません。不具合が発生したら報告してください。", - "xpack.apm.settings.schema.migrate.dataStreams.betaBadge.label": "ベータ", - "xpack.apm.settings.schema.migrate.dataStreams.betaBadge.title": "データストリーム", "xpack.apm.settings.schema.migrate.dataStreams.buttonText": "データストリームに切り替える", "xpack.apm.settings.schema.migrate.dataStreams.description": "今後、新しく取り込まれたデータはすべてデータストリームに格納されます。以前に取り込まれたデータはクラシックAPMインデックスに残ります。APMおよびUXアプリは引き続き両方のインデックスをサポートします。", "xpack.apm.settings.schema.migrate.dataStreams.title": "データストリーム", @@ -9362,11 +9325,9 @@ "xpack.enterpriseSearch.appSearch.engine.curations.manageQueryButtonLabel": "クエリを管理", "xpack.enterpriseSearch.appSearch.engine.curations.manageQueryDescription": "このキュレーションのクエリを編集、追加、削除します。", "xpack.enterpriseSearch.appSearch.engine.curations.manageQueryTitle": "クエリを管理", - "xpack.enterpriseSearch.appSearch.engine.curations.organicDocuments.emptyDescription": "表示するオーガニック結果はありません。上記のアクティブなクエリを追加または変更します。", "xpack.enterpriseSearch.appSearch.engine.curations.organicDocuments.title": "\"{currentQuery}\"の上位のオーガニックドキュメント", "xpack.enterpriseSearch.appSearch.engine.curations.overview.title": "キュレーションされた結果", "xpack.enterpriseSearch.appSearch.engine.curations.promoteButtonLabel": "この結果を昇格", - "xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.description": "昇格された結果はオーガニック結果の前に表示されます。ドキュメントを並べ替えることができます。", "xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.emptyDescription": "以下のオーガニック結果からドキュメントにスターを付けるか、手動で結果を検索して昇格します。", "xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.removeAllButtonLabel": "すべて降格", "xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.title": "昇格されたドキュメント", @@ -18947,36 +18908,19 @@ "xpack.observability.expView.operationType.95thPercentile": "95パーセンタイル", "xpack.observability.expView.operationType.99thPercentile": "99パーセンタイル", "xpack.observability.expView.operationType.average": "平均", - "xpack.observability.expView.operationType.label": "計算", "xpack.observability.expView.operationType.median": "中央", "xpack.observability.expView.operationType.sum": "合計", - "xpack.observability.expView.reportType.noDataType": "データ型が選択されていません。", "xpack.observability.expView.reportType.selectDataType": "ビジュアライゼーションを作成するデータ型を選択します。", - "xpack.observability.expView.seriesBuilder.actions": "アクション", "xpack.observability.expView.seriesBuilder.addSeries": "数列を追加", "xpack.observability.expView.seriesBuilder.apply": "変更を適用", - "xpack.observability.expView.seriesBuilder.autoApply": "自動適用", - "xpack.observability.expView.seriesBuilder.breakdown": "内訳", - "xpack.observability.expView.seriesBuilder.dataType": "データ型", - "xpack.observability.expView.seriesBuilder.definition": "定義", "xpack.observability.expView.seriesBuilder.emptyReportDefinition": "ビジュアライゼーションを作成するレポート定義を選択します。", "xpack.observability.expView.seriesBuilder.emptyview": "表示する情報がありません。", - "xpack.observability.expView.seriesBuilder.filters": "フィルター", "xpack.observability.expView.seriesBuilder.loadingView": "ビューを読み込んでいます...", - "xpack.observability.expView.seriesBuilder.report": "レポート", - "xpack.observability.expView.seriesBuilder.selectDataType": "データ型が選択されていません", "xpack.observability.expView.seriesBuilder.selectReportType": "レポートタイプが選択されていません", "xpack.observability.expView.seriesBuilder.selectReportType.empty": "レポートタイプを選択すると、ビジュアライゼーションを作成します。", - "xpack.observability.expView.seriesEditor.actions": "アクション", - "xpack.observability.expView.seriesEditor.addFilter": "フィルターを追加します", - "xpack.observability.expView.seriesEditor.breakdowns": "内訳", "xpack.observability.expView.seriesEditor.clearFilter": "フィルターを消去", - "xpack.observability.expView.seriesEditor.filters": "フィルター", - "xpack.observability.expView.seriesEditor.name": "名前", "xpack.observability.expView.seriesEditor.notFound": "系列が見つかりません。系列を追加してください。", "xpack.observability.expView.seriesEditor.removeSeries": "クリックすると、系列を削除します", - "xpack.observability.expView.seriesEditor.seriesNotFound": "系列が見つかりません。系列を追加してください。", - "xpack.observability.expView.seriesEditor.time": "時間", "xpack.observability.featureCatalogueDescription": "専用UIで、ログ、メトリック、アプリケーショントレース、システム可用性を連結します。", "xpack.observability.featureCatalogueTitle": "オブザーバビリティ", "xpack.observability.featureRegistry.linkObservabilityTitle": "ケース", @@ -19038,7 +18982,6 @@ "xpack.observability.overview.ux.title": "ユーザーエクスペリエンス", "xpack.observability.overviewLinkTitle": "概要", "xpack.observability.pageLayout.sideNavTitle": "オブザーバビリティ", - "xpack.observability.reportTypeCol.nodata": "利用可能なデータがありません", "xpack.observability.resources.documentation": "ドキュメント", "xpack.observability.resources.forum": "ディスカッションフォーラム", "xpack.observability.resources.quick_start": "クイックスタートビデオ", @@ -19054,8 +18997,6 @@ "xpack.observability.section.apps.uptime.title": "アップタイム", "xpack.observability.section.errorPanel": "データの取得時にエラーが発生しました。再試行してください", "xpack.observability.seriesEditor.clone": "系列をコピー", - "xpack.observability.seriesEditor.edit": "系列を編集", - "xpack.observability.seriesEditor.save": "系列を保存", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.ux.coreVitals.average": "平均", "xpack.observability.ux.coreVitals.averageMessage": " {bad}未満", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e3f53a34449e..f2e5308bd6c5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1669,12 +1669,12 @@ "data.functions.esaggs.help": "运行 AggConfig 聚合", "data.functions.esaggs.inspector.dataRequest.description": "此请求查询 Elasticsearch,以获取可视化的数据。", "data.functions.esaggs.inspector.dataRequest.title": "数据", - "data.functions.indexPatternLoad.help": "加载索引模式", - "data.functions.indexPatternLoad.id.help": "要加载的索引模式 id", - "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "要在 Kibana 中可视化和浏览数据,必须创建索引模式,以从 Elasticsearch 中检索数据。", - "data.indexPatterns.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", - "data.indexPatterns.indexPatternLoad.error.kibanaRequest": "在服务器上执行此搜索时需要 Kibana 请求。请向表达式执行模式参数提供请求对象。", - "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", + "dataViews.indexPatternLoad.help": "加载索引模式", + "dataViews.functions.indexPatternLoad.id.help": "要加载的索引模式 id", + "dataViews.ensureDefaultIndexPattern.bannerLabel": "要在 Kibana 中可视化和浏览数据,必须创建索引模式,以从 Elasticsearch 中检索数据。", + "dataViews.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", + "dataViews.indexPatternLoad.error.kibanaRequest": "在服务器上执行此搜索时需要 Kibana 请求。请向表达式执行模式参数提供请求对象。", + "dataViews.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", "data.inspector.table..dataDescriptionTooltip": "查看可视化后面的数据", "data.inspector.table.dataTitle": "数据", "data.inspector.table.downloadCSVToggleButtonLabel": "下载 CSV", @@ -5241,12 +5241,6 @@ "visTypeTimeseries.indexPattern.timeRange.lastValue": "最后值", "visTypeTimeseries.indexPattern.timeRange.selectTimeRange": "选择", "visTypeTimeseries.indexPattern.сoarse": "粗糙", - "visTypeTimeseries.indexPatternSelect.createIndexPatternText": "创建索引模式", - "visTypeTimeseries.indexPatternSelect.defaultIndexPatternText": "将使用默认索引模式。", - "visTypeTimeseries.indexPatternSelect.label": "索引模式", - "visTypeTimeseries.indexPatternSelect.queryAllIndexesText": "要查询所有索引,请使用 *", - "visTypeTimeseries.indexPatternSelect.switchModePopover.areaLabel": "配置索引模式选择模式", - "visTypeTimeseries.indexPatternSelect.switchModePopover.title": "索引模式选择模式", "visTypeTimeseries.kbnVisTypes.metricsDescription": "对时间序列数据执行高级分析。", "visTypeTimeseries.kbnVisTypes.metricsTitle": "TSVB", "visTypeTimeseries.lastValueModeIndicator.lastBucketDate": "存储桶:{lastBucketDate}", @@ -5376,7 +5370,6 @@ "visTypeTimeseries.seriesConfig.ignoreGlobalFilterLabel": "忽略全局筛选?", "visTypeTimeseries.seriesConfig.missingSeriesComponentDescription": "以下面板类型缺失序列组件:{panelType}", "visTypeTimeseries.seriesConfig.offsetSeriesTimeLabel": "将序列时间偏移(1m、1h、1w、1d)", - "visTypeTimeseries.seriesConfig.overrideIndexPatternLabel": "覆盖索引模式?", "visTypeTimeseries.seriesConfig.templateHelpText": "例如 {templateExample}", "visTypeTimeseries.seriesConfig.templateLabel": "模板", "visTypeTimeseries.sort.dragToSortAriaLabel": "拖动以排序", @@ -5507,7 +5500,6 @@ "visTypeTimeseries.timeseries.optionsTab.styleLabel": "样式", "visTypeTimeseries.timeseries.optionsTab.tooltipMode": "工具提示", "visTypeTimeseries.timeseries.optionsTab.truncateLegendLabel": "截断图例?", - "visTypeTimeseries.timeSeries.overrideIndexPatternLabel": "覆盖索引模式?", "visTypeTimeseries.timeSeries.percentLabel": "百分比", "visTypeTimeseries.timeseries.positionOptions.leftLabel": "左", "visTypeTimeseries.timeseries.positionOptions.rightLabel": "右", @@ -5572,9 +5564,8 @@ "visTypeTimeseries.visEditorVisualization.changesHaveNotBeenAppliedMessage": "尚未应用对此可视化的更改。", "visTypeTimeseries.visEditorVisualization.changesSuccessfullyAppliedMessage": "已应用最新更改。", "visTypeTimeseries.visEditorVisualization.changesWillBeAutomaticallyAppliedMessage": "将自动应用更改。", - "visTypeTimeseries.visEditorVisualization.indexPatternMode.dismissNoticeButtonText": "关闭", - "visTypeTimeseries.visEditorVisualization.indexPatternMode.link": "请查看。", - "visTypeTimeseries.visEditorVisualization.indexPatternMode.notificationTitle": "TSVB 现在支持索引模式", + "visTypeTimeseries.visEditorVisualization.dataViewMode.dismissNoticeButtonText": "关闭", + "visTypeTimeseries.visEditorVisualization.dataViewMode.link": "请查看。", "visTypeTimeseries.visPicker.gaugeLabel": "仪表盘", "visTypeTimeseries.visPicker.metricLabel": "指标", "visTypeTimeseries.visPicker.tableLabel": "表", @@ -6368,9 +6359,6 @@ "xpack.apm.exactTransactionRateLabel": "{value} { unit, select, minute {tpm} other {tps} }", "xpack.apm.failedTransactionsCorrelations.licenseCheckText": "要使用失败事务相关性功能,必须订阅 Elastic 白金级许可证。", "xpack.apm.featureRegistry.apmFeatureName": "APM 和用户体验", - "xpack.apm.featureRegistry.manageAlertsName": "告警", - "xpack.apm.featureRegistry.subfeature.alertsAllName": "全部", - "xpack.apm.featureRegistry.subfeature.alertsReadName": "读取", "xpack.apm.feedbackMenu.appName": "APM", "xpack.apm.fetcher.error.status": "错误", "xpack.apm.fetcher.error.title": "提取资源时出错", @@ -6492,24 +6480,6 @@ "xpack.apm.localFilters.titles.serviceName": "服务名称", "xpack.apm.localFilters.titles.transactionUrl": "URL", "xpack.apm.localFiltersTitle": "筛选", - "xpack.apm.metadataTable.section.agentLabel": "代理", - "xpack.apm.metadataTable.section.clientLabel": "客户端", - "xpack.apm.metadataTable.section.containerLabel": "容器", - "xpack.apm.metadataTable.section.customLabel": "定制", - "xpack.apm.metadataTable.section.errorLabel": "错误", - "xpack.apm.metadataTable.section.hostLabel": "主机", - "xpack.apm.metadataTable.section.httpLabel": "HTTP", - "xpack.apm.metadataTable.section.labelsLabel": "标签", - "xpack.apm.metadataTable.section.messageLabel": "消息", - "xpack.apm.metadataTable.section.pageLabel": "页", - "xpack.apm.metadataTable.section.processLabel": "进程", - "xpack.apm.metadataTable.section.serviceLabel": "服务", - "xpack.apm.metadataTable.section.spanLabel": "跨度", - "xpack.apm.metadataTable.section.traceLabel": "跟踪", - "xpack.apm.metadataTable.section.transactionLabel": "事务", - "xpack.apm.metadataTable.section.urlLabel": "URL", - "xpack.apm.metadataTable.section.userAgentLabel": "用户代理", - "xpack.apm.metadataTable.section.userLabel": "用户", "xpack.apm.metrics.transactionChart.machineLearningLabel": "Machine Learning", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "流显示平均延迟的预期边界。红色垂直标注表示异常分数等于或大于 75 的异常。", "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "使用搜索栏筛选时,Machine Learning 结果处于隐藏状态", @@ -6824,10 +6794,6 @@ "xpack.apm.settings.schema.confirm.title": "请确认您的选择", "xpack.apm.settings.schema.confirm.unsupportedConfigs.descriptionText": "系统会替您将兼容的定制 apm-server.yml 用户设置移到 Fleet 服务器设置。我们将会让您了解哪些设置不兼容后,才会移除它们。", "xpack.apm.settings.schema.confirm.unsupportedConfigs.title": "以下 apm-server.yml 用户设置不兼容,将会被移除", - "xpack.apm.settings.schema.descriptionText": "从经典 APM 索引切换是简单且无缝的过程,让您可以立即利用新的数据流功能。注意,此操作{irreversibleEmphasis},且只能由对 Fleet 具有访问权限的{superuserEmphasis}执行。详细了解{dataStreamsDocLink}。", - "xpack.apm.settings.schema.descriptionText.betaCalloutMessage": "此功能为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", - "xpack.apm.settings.schema.descriptionText.betaCalloutTitle": "数据流在 APM 中为公测版", - "xpack.apm.settings.schema.descriptionText.dataStreamsDocLinkText": "数据流", "xpack.apm.settings.schema.descriptionText.irreversibleEmphasisText": "不可逆", "xpack.apm.settings.schema.descriptionText.superuserEmphasisText": "超级用户", "xpack.apm.settings.schema.disabledReason": "无法切换到数据流:{reasons}", @@ -6837,9 +6803,6 @@ "xpack.apm.settings.schema.migrate.classicIndices.currentSetup": "当前设置", "xpack.apm.settings.schema.migrate.classicIndices.description": "您当前正将经典 APM 索引用于您的数据。此数据架构将退役,将在 Elastic Stack 版本 8.0 中由数据流替代。", "xpack.apm.settings.schema.migrate.classicIndices.title": "经典 APM 索引", - "xpack.apm.settings.schema.migrate.dataStreams.betaBadge.description": "切换到数据流尚未正式发布。请通过报告错误来帮助我们。", - "xpack.apm.settings.schema.migrate.dataStreams.betaBadge.label": "公测版", - "xpack.apm.settings.schema.migrate.dataStreams.betaBadge.title": "数据流", "xpack.apm.settings.schema.migrate.dataStreams.buttonText": "切换到数据流", "xpack.apm.settings.schema.migrate.dataStreams.description": "将来,任何新采集的数据都将存储在数据流中。之前采集的数据仍在经典 APM 索引中。APM 和 UX 应用将继续支持这两种索引。", "xpack.apm.settings.schema.migrate.dataStreams.title": "数据流", @@ -9454,11 +9417,9 @@ "xpack.enterpriseSearch.appSearch.engine.curations.manageQueryButtonLabel": "管理查询", "xpack.enterpriseSearch.appSearch.engine.curations.manageQueryDescription": "编辑、添加或移除此策展的查询。", "xpack.enterpriseSearch.appSearch.engine.curations.manageQueryTitle": "管理查询", - "xpack.enterpriseSearch.appSearch.engine.curations.organicDocuments.emptyDescription": "没有要显示的有机结果。在上面添加或更改活动查询。", "xpack.enterpriseSearch.appSearch.engine.curations.organicDocuments.title": "“{currentQuery}”的排名靠前有机文档", "xpack.enterpriseSearch.appSearch.engine.curations.overview.title": "已策展结果", "xpack.enterpriseSearch.appSearch.engine.curations.promoteButtonLabel": "提升此结果", - "xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.description": "提升结果显示在有机结果之前。可以重新排列文档。", "xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.emptyDescription": "使用星号标记来自下面有机结果的文档或手动搜索或提升结果。", "xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.removeAllButtonLabel": "全部降低", "xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.title": "提升文档", @@ -19220,36 +19181,19 @@ "xpack.observability.expView.operationType.95thPercentile": "第 95 个百分位", "xpack.observability.expView.operationType.99thPercentile": "第 99 个百分位", "xpack.observability.expView.operationType.average": "平均值", - "xpack.observability.expView.operationType.label": "计算", "xpack.observability.expView.operationType.median": "中值", "xpack.observability.expView.operationType.sum": "求和", - "xpack.observability.expView.reportType.noDataType": "未选择任何数据类型。", "xpack.observability.expView.reportType.selectDataType": "选择数据类型以创建可视化。", - "xpack.observability.expView.seriesBuilder.actions": "操作", "xpack.observability.expView.seriesBuilder.addSeries": "添加序列", "xpack.observability.expView.seriesBuilder.apply": "应用更改", - "xpack.observability.expView.seriesBuilder.autoApply": "自动应用", - "xpack.observability.expView.seriesBuilder.breakdown": "分解", - "xpack.observability.expView.seriesBuilder.dataType": "数据类型", - "xpack.observability.expView.seriesBuilder.definition": "定义", "xpack.observability.expView.seriesBuilder.emptyReportDefinition": "选择报告定义以创建可视化。", "xpack.observability.expView.seriesBuilder.emptyview": "没有可显示的内容。", - "xpack.observability.expView.seriesBuilder.filters": "筛选", "xpack.observability.expView.seriesBuilder.loadingView": "正在加载视图......", - "xpack.observability.expView.seriesBuilder.report": "报告", - "xpack.observability.expView.seriesBuilder.selectDataType": "未选择任何数据类型", "xpack.observability.expView.seriesBuilder.selectReportType": "未选择任何报告类型", "xpack.observability.expView.seriesBuilder.selectReportType.empty": "选择报告类型以创建可视化。", - "xpack.observability.expView.seriesEditor.actions": "操作", - "xpack.observability.expView.seriesEditor.addFilter": "添加筛选", - "xpack.observability.expView.seriesEditor.breakdowns": "分解", "xpack.observability.expView.seriesEditor.clearFilter": "清除筛选", - "xpack.observability.expView.seriesEditor.filters": "筛选", - "xpack.observability.expView.seriesEditor.name": "名称", "xpack.observability.expView.seriesEditor.notFound": "未找到任何序列。请添加序列。", "xpack.observability.expView.seriesEditor.removeSeries": "单击移除序列", - "xpack.observability.expView.seriesEditor.seriesNotFound": "未找到任何序列。请添加序列。", - "xpack.observability.expView.seriesEditor.time": "时间", "xpack.observability.featureCatalogueDescription": "通过专用 UI 整合您的日志、指标、应用程序跟踪和系统可用性。", "xpack.observability.featureCatalogueTitle": "可观测性", "xpack.observability.featureRegistry.linkObservabilityTitle": "案例", @@ -19311,7 +19255,6 @@ "xpack.observability.overview.ux.title": "用户体验", "xpack.observability.overviewLinkTitle": "概览", "xpack.observability.pageLayout.sideNavTitle": "可观测性", - "xpack.observability.reportTypeCol.nodata": "没有可用数据", "xpack.observability.resources.documentation": "文档", "xpack.observability.resources.forum": "讨论论坛", "xpack.observability.resources.quick_start": "快速入门视频", @@ -19327,8 +19270,6 @@ "xpack.observability.section.apps.uptime.title": "运行时间", "xpack.observability.section.errorPanel": "尝试提取数据时发生错误。请重试", "xpack.observability.seriesEditor.clone": "复制序列", - "xpack.observability.seriesEditor.edit": "编辑序列", - "xpack.observability.seriesEditor.save": "保存序列", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.ux.coreVitals.average": "平均值", "xpack.observability.ux.coreVitals.averageMessage": " 且小于 {bad}", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx index a05db00f141a..812c234e80d9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx @@ -95,6 +95,10 @@ describe('JiraParamsFields renders', () => { description: { allowedValues: [], defaultValue: {} }, }, }; + const useGetFieldsByIssueTypeResponseLoading = { + isLoading: true, + fields: {}, + }; beforeEach(() => { jest.clearAllMocks(); @@ -421,5 +425,19 @@ describe('JiraParamsFields renders', () => { expect(editAction.mock.calls[0][1].incident.priority).toEqual('Medium'); expect(editAction.mock.calls[1][1].incident.priority).toEqual(null); }); + + test('Preserve priority when the issue type fields are loading and hasPriority becomes stale', () => { + useGetFieldsByIssueTypeMock + .mockReturnValueOnce(useGetFieldsByIssueTypeResponseLoading) + .mockReturnValue(useGetFieldsByIssueTypeResponse); + const wrapper = mount(); + + expect(editAction).not.toBeCalled(); + + wrapper.setProps({ ...defaultProps }); // just to force component call useGetFieldsByIssueType again + + expect(editAction).toBeCalledTimes(1); + expect(editAction.mock.calls[0][1].incident.priority).toEqual('Medium'); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 834892f2bf37..32390c163cf2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -147,11 +147,11 @@ const JiraParamsFields: React.FunctionComponent { - if (!hasPriority && incident.priority != null) { + if (!isLoadingFields && !hasPriority && incident.priority != null) { editSubActionProperty('priority', null); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hasPriority]); + }, [hasPriority, isLoadingFields]); const labelOptions = useMemo( () => (incident.labels ? incident.labels.map((label: string) => ({ label })) : []), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx index 38be618119c4..61db73c129db 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx @@ -62,8 +62,8 @@ export const useGetFieldsByIssueType = ({ }); if (!didCancel) { - setIsLoading(false); setFields(res.data ?? {}); + setIsLoading(false); if (res.status && res.status === 'error') { toastNotifications.addDanger({ title: i18n.FIELDS_API_ERROR, diff --git a/x-pack/plugins/uptime/common/runtime_types/network_events.ts b/x-pack/plugins/uptime/common/runtime_types/network_events.ts index d24014aec8ea..0b717f46cedc 100644 --- a/x-pack/plugins/uptime/common/runtime_types/network_events.ts +++ b/x-pack/plugins/uptime/common/runtime_types/network_events.ts @@ -56,6 +56,7 @@ export const SyntheticsNetworkEventsApiResponseType = t.type({ events: t.array(NetworkEventType), total: t.number, isWaterfallSupported: t.boolean, + hasNavigationRequest: t.boolean, }); export type SyntheticsNetworkEventsApiResponse = t.TypeOf< diff --git a/x-pack/plugins/uptime/public/apps/uptime_page_template.tsx b/x-pack/plugins/uptime/public/apps/uptime_page_template.tsx index 829f587e248e..033fdcb61b28 100644 --- a/x-pack/plugins/uptime/public/apps/uptime_page_template.tsx +++ b/x-pack/plugins/uptime/public/apps/uptime_page_template.tsx @@ -38,12 +38,14 @@ export const UptimePageTemplateComponent: React.FC = ({ path, pageHeader, const noDataConfig = useNoDataConfig(); - const { loading, error } = useHasData(); + const { loading, error, data } = useHasData(); if (error) { return ; } + const showLoading = loading && path === OVERVIEW_ROUTE && !data; + return ( <>
@@ -51,9 +53,9 @@ export const UptimePageTemplateComponent: React.FC = ({ path, pageHeader, pageHeader={pageHeader} noDataConfig={path === OVERVIEW_ROUTE && !loading ? noDataConfig : undefined} > - {loading && path === OVERVIEW_ROUTE && } + {showLoading && }
{children} diff --git a/x-pack/plugins/uptime/public/components/certificates/__snapshots__/fingerprint_col.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__snapshots__/fingerprint_col.test.tsx.snap deleted file mode 100644 index 33969b3d83bc..000000000000 --- a/x-pack/plugins/uptime/public/components/certificates/__snapshots__/fingerprint_col.test.tsx.snap +++ /dev/null @@ -1,202 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FingerprintCol renders expected elements for valid props 1`] = ` -.c1 .euiButtonEmpty__content { - padding-right: 0px; -} - -.c0 { - margin-right: 8px; -} - - - - - - - - - - - - - - - - - - - -`; - -exports[`FingerprintCol shallow renders expected elements for valid props 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx index 1affd1f990f9..f3f4e206f6e2 100644 --- a/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; -import { renderWithRouter, shallowWithRouter } from '../../lib'; -import { FingerprintCol } from './fingerprint_col'; import moment from 'moment'; +import { FingerprintCol } from './fingerprint_col'; +import { render } from '../../lib/helper/rtl_helpers'; describe('FingerprintCol', () => { const cert = { @@ -16,18 +16,19 @@ describe('FingerprintCol', () => { not_after: '2020-05-08T00:00:00.000Z', not_before: '2018-05-08T00:00:00.000Z', issuer: 'DigiCert SHA2 Extended Validation Server CA', - sha1: 'ca06f56b258b7a0d4f2b05470939478651151984', - sha256: '3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074', + sha1: 'ca06f56b258b7a0d4f2b05470939478651151984'.toUpperCase(), + sha256: '3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074'.toUpperCase(), common_name: 'github.com', }; - it('shallow renders expected elements for valid props', () => { - expect(shallowWithRouter()).toMatchSnapshot(); - }); - - it('renders expected elements for valid props', () => { + it('renders expected elements for valid props', async () => { cert.not_after = moment().add('4', 'months').toISOString(); + const { findByText, findByTestId } = render(); + + expect(await findByText('SHA 1')).toBeInTheDocument(); + expect(await findByText('SHA 256')).toBeInTheDocument(); - expect(renderWithRouter()).toMatchSnapshot(); + expect(await findByTestId(cert.sha1)).toBeInTheDocument(); + expect(await findByTestId(cert.sha256)).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/common/__snapshots__/uptime_date_picker.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/__snapshots__/uptime_date_picker.test.tsx.snap deleted file mode 100644 index d513c0212da7..000000000000 --- a/x-pack/plugins/uptime/public/components/common/__snapshots__/uptime_date_picker.test.tsx.snap +++ /dev/null @@ -1,184 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UptimeDatePicker component renders properly with mock data 1`] = ` -
-
-
-
-
- -
-
-
-
- -
-
-
-
-
- - - -
-
-`; - -exports[`UptimeDatePicker component validates props with shallow render 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx index 1a53a2c9b64a..aa981071b7ee 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -22,6 +22,7 @@ import React, { useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import numeral from '@elastic/numeral'; import moment from 'moment'; +import { useSelector } from 'react-redux'; import { getChartDateLabel } from '../../../lib/helper'; import { ChartWrapper } from './chart_wrapper'; import { UptimeThemeContext } from '../../../contexts'; @@ -32,6 +33,7 @@ import { getDateRangeFromChartElement } from './utils'; import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations'; import { createExploratoryViewUrl } from '../../../../../observability/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; +import { monitorStatusSelector } from '../../../state/selectors'; export interface PingHistogramComponentProps { /** @@ -73,6 +75,8 @@ export const PingHistogramComponent: React.FC = ({ const monitorId = useMonitorId(); + const selectedMonitor = useSelector(monitorStatusSelector); + const { basePath } = useUptimeSettingsContext(); const [getUrlParams, updateUrlParams] = useUrlParams(); @@ -189,12 +193,21 @@ export const PingHistogramComponent: React.FC = ({ const pingHistogramExploratoryViewLink = createExploratoryViewUrl( { - 'pings-over-time': { - dataType: 'synthetics', - reportType: 'kpi-over-time', - time: { from: dateRangeStart, to: dateRangeEnd }, - ...(monitorId ? { filters: [{ field: 'monitor.id', values: [monitorId] }] } : {}), - }, + reportType: 'kpi-over-time', + allSeries: [ + { + name: `${monitorId}-pings`, + dataType: 'synthetics', + selectedMetricField: 'summary.up', + time: { from: dateRangeStart, to: dateRangeEnd }, + reportDefinitions: { + 'monitor.name': + monitorId && selectedMonitor?.monitor?.name + ? [selectedMonitor.monitor.name] + : ['ALL_VALUES'], + }, + }, + ], }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index ef5e10394739..c459fe46da97 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -10,13 +10,15 @@ import { EuiHeaderLinks, EuiToolTip, EuiHeaderLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; -import { createExploratoryViewUrl, SeriesUrl } from '../../../../../observability/public'; +import { useSelector } from 'react-redux'; +import { createExploratoryViewUrl } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; import { useGetUrlParams } from '../../../hooks'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; import { SETTINGS_ROUTE } from '../../../../common/constants'; import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; +import { monitorStatusSelector } from '../../../state/selectors'; const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', { defaultMessage: 'Add data', @@ -38,13 +40,28 @@ export function ActionMenuContent(): React.ReactElement { const { dateRangeStart, dateRangeEnd } = params; const history = useHistory(); + const selectedMonitor = useSelector(monitorStatusSelector); + + const monitorId = selectedMonitor?.monitor?.id; + const syntheticExploratoryViewLink = createExploratoryViewUrl( { - 'synthetics-series': { - dataType: 'synthetics', - isNew: true, - time: { from: dateRangeStart, to: dateRangeEnd }, - } as unknown as SeriesUrl, + reportType: 'kpi-over-time', + allSeries: [ + { + dataType: 'synthetics', + seriesType: 'area_stacked', + selectedMetricField: 'monitor.duration.us', + time: { from: dateRangeStart, to: dateRangeEnd }, + breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', + reportDefinitions: { + 'monitor.name': selectedMonitor?.monitor?.name + ? [selectedMonitor?.monitor?.name] + : ['ALL_VALUES'], + }, + name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration', + }, + ], }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx index f1b37d0c98f4..997831442faa 100644 --- a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx @@ -7,52 +7,33 @@ import React from 'react'; import { UptimeDatePicker } from './uptime_date_picker'; -import { - renderWithRouter, - shallowWithRouter, - MountWithReduxProvider, - mountWithRouterRedux, -} from '../../lib'; -import { UptimeStartupPluginsContextProvider } from '../../contexts'; import { startPlugins } from '../../lib/__mocks__/uptime_plugin_start_mock'; -import { ClientPluginsStart } from '../../apps/plugin'; import { createMemoryHistory } from 'history'; +import { render } from '../../lib/helper/rtl_helpers'; +import { fireEvent } from '@testing-library/dom'; describe('UptimeDatePicker component', () => { - it('validates props with shallow render', () => { - const component = shallowWithRouter(); - expect(component).toMatchSnapshot(); + it('renders properly with mock data', async () => { + const { findByText } = render(); + expect(await findByText('Last 15 minutes')).toBeInTheDocument(); + expect(await findByText('Refresh')).toBeInTheDocument(); }); - it('renders properly with mock data', () => { - const component = renderWithRouter( - - - - ); - expect(component).toMatchSnapshot(); - }); + it('uses shared date range state when there is no url date range state', async () => { + const customHistory = createMemoryHistory({ + initialEntries: ['/?dateRangeStart=now-15m&dateRangeEnd=now'], + }); - it('uses shared date range state when there is no url date range state', () => { - const customHistory = createMemoryHistory(); jest.spyOn(customHistory, 'push'); - const component = mountWithRouterRedux( - )} - > - - , - { customHistory } - ); - - const startBtn = component.find('[data-test-subj="superDatePickerstartDatePopoverButton"]'); - - expect(startBtn.text()).toBe('~ 30 minutes ago'); + const { findByText } = render(, { + history: customHistory, + core: startPlugins, + }); - const endBtn = component.find('[data-test-subj="superDatePickerendDatePopoverButton"]'); + expect(await findByText('~ 15 minutes ago')).toBeInTheDocument(); - expect(endBtn.text()).toBe('~ 15 minutes ago'); + expect(await findByText('~ 30 minutes ago')).toBeInTheDocument(); expect(customHistory.push).toHaveBeenCalledWith({ pathname: '/', @@ -60,31 +41,60 @@ describe('UptimeDatePicker component', () => { }); }); - it('should use url date range even if shared date range is present', () => { + it('should use url date range even if shared date range is present', async () => { + const customHistory = createMemoryHistory({ + initialEntries: ['/?g=%22%22&dateRangeStart=now-10m&dateRangeEnd=now'], + }); + + jest.spyOn(customHistory, 'push'); + + const { findByText } = render(, { + history: customHistory, + core: startPlugins, + }); + + expect(await findByText('Last 10 minutes')).toBeInTheDocument(); + + // it should update shared state + + expect(startPlugins.data.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({ + from: 'now-10m', + to: 'now', + }); + }); + + it('should handle on change', async () => { const customHistory = createMemoryHistory({ initialEntries: ['/?g=%22%22&dateRangeStart=now-10m&dateRangeEnd=now'], }); jest.spyOn(customHistory, 'push'); - const component = mountWithRouterRedux( - )} - > - - , - { customHistory } - ); + const { findByText, getByTestId, findByTestId } = render(, { + history: customHistory, + core: startPlugins, + }); + + expect(await findByText('Last 10 minutes')).toBeInTheDocument(); - const showDateBtn = component.find('[data-test-subj="superDatePickerShowDatesButton"]'); + fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton')); - expect(showDateBtn.childAt(0).text()).toBe('Last 10 minutes'); + fireEvent.click(await findByTestId('superDatePickerCommonlyUsed_Today')); + + expect(await findByText('Today')).toBeInTheDocument(); // it should update shared state + expect(startPlugins.data.query.timefilter.timefilter.setTime).toHaveBeenCalledTimes(3); + expect(startPlugins.data.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({ from: 'now-10m', to: 'now', }); + + expect(startPlugins.data.query.timefilter.timefilter.setTime).toHaveBeenLastCalledWith({ + from: 'now/d', + to: 'now', + }); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/license_info.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/license_info.test.tsx.snap deleted file mode 100644 index 98414f82bf19..000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/license_info.test.tsx.snap +++ /dev/null @@ -1,78 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ShowLicenseInfo renders without errors 1`] = ` -Array [ -
-
-
-
-
-

- In order to access duration anomaly detection, you have to be subscribed to an Elastic Platinum license. -

- - - - Start free 14-day trial - - - -
-
-
, -
, -] -`; - -exports[`ShowLicenseInfo shallow renders without errors 1`] = ` - - -

- In order to access duration anomaly detection, you have to be subscribed to an Elastic Platinum license. -

- - Start free 14-day trial - -
- -
-`; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap deleted file mode 100644 index e4672338485f..000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap +++ /dev/null @@ -1,242 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ML Flyout component renders without errors 1`] = ` - - - -

- Enable anomaly detection -

-
- -
- - -

- Here you can create a machine learning job to calculate anomaly scores on - response durations for Uptime Monitor. Once enabled, the monitor duration chart on the details page - will show the expected bounds and annotate the graph with anomalies. You can also potentially - identify periods of increased latency across geographical regions. -

-

- - Machine Learning jobs management page - , - } - } - /> -

-

- - Note: It might take a few minutes for the job to begin calculating results. - -

-
- -
- - - - - Cancel - - - - - Create new job - - - - -
-`; - -exports[`ML Flyout component shows license info if no ml available 1`] = ` -
- -
-
- -
-
-
-
-`; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/license_info.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/license_info.test.tsx index aa175715970c..f8e0c44c2b8c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/license_info.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/license_info.test.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; -import { renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { ShowLicenseInfo } from './license_info'; import * as redux from 'react-redux'; +import { render } from '../../../lib/helper/rtl_helpers'; describe('ShowLicenseInfo', () => { beforeEach(() => { @@ -18,13 +18,9 @@ describe('ShowLicenseInfo', () => { const spy1 = jest.spyOn(redux, 'useSelector'); spy1.mockReturnValue(true); }); - it('shallow renders without errors', () => { - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); - }); - it('renders without errors', () => { - const wrapper = renderWithIntl(); - expect(wrapper).toMatchSnapshot(); + it('renders without errors', async () => { + const { findAllByText } = render(); + expect((await findAllByText('Start free 14-day trial'))[0]).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.test.tsx index 200087976bc8..d066bf416e08 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.test.tsx @@ -6,11 +6,12 @@ */ import React from 'react'; -import { renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { MLFlyoutView } from './ml_flyout'; import { UptimeSettingsContext } from '../../../contexts'; import { CLIENT_DEFAULTS } from '../../../../common/constants'; import * as redux from 'react-redux'; +import { render } from '../../../lib/helper/rtl_helpers'; +import * as labels from './translations'; describe('ML Flyout component', () => { const createJob = () => {}; @@ -25,18 +26,7 @@ describe('ML Flyout component', () => { spy1.mockReturnValue(true); }); - it('renders without errors', () => { - const wrapper = shallowWithIntl( - - ); - expect(wrapper).toMatchSnapshot(); - }); - it('shows license info if no ml available', () => { + it('shows license info if no ml available', async () => { const spy1 = jest.spyOn(redux, 'useSelector'); // return false value for no license @@ -50,7 +40,7 @@ describe('ML Flyout component', () => { isInfraAvailable: true, isLogsAvailable: true, }; - const wrapper = renderWithIntl( + const { findByText, findAllByText } = render( { /> ); - const licenseComponent = wrapper.find('.license-info-trial'); - expect(licenseComponent.length).toBe(1); - expect(wrapper).toMatchSnapshot(); + + expect(await findByText(labels.ENABLE_ANOMALY_DETECTION)).toBeInTheDocument(); + expect(await findAllByText(labels.START_TRAIL)).toHaveLength(2); }); - it('able to create job if valid license is available', () => { + it('able to create job if valid license is available', async () => { const value = { basePath: '', dateRangeStart: DATE_RANGE_START, @@ -74,7 +64,7 @@ describe('ML Flyout component', () => { isInfraAvailable: true, isLogsAvailable: true, }; - const wrapper = renderWithIntl( + const { queryByText } = render( { ); - const licenseComponent = wrapper.find('.license-info-trial'); - expect(licenseComponent.length).toBe(0); + expect(queryByText(labels.START_TRAIL)).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx index 478edb563df9..c1e32613a2ff 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx @@ -41,6 +41,7 @@ const showMLJobNotification = ( basePath: string, range: { to: string; from: string }, success: boolean, + awaitingNodeAssignment: boolean, error?: Error ) => { if (success) { @@ -51,7 +52,9 @@ const showMLJobNotification = ( ), text: toMountPoint(

- {labels.JOB_CREATED_SUCCESS_MESSAGE} + {awaitingNodeAssignment + ? labels.JOB_CREATED_LAZY_SUCCESS_MESSAGE + : labels.JOB_CREATED_SUCCESS_MESSAGE} {labels.VIEW_JOB} @@ -107,7 +110,8 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => { monitorId as string, basePath, { to: dateRangeEnd, from: dateRangeStart }, - true + true, + hasMLJob.awaitingNodeAssignment ); const loadMLJob = (jobId: string) => dispatch(getExistingMLJobAction.get({ monitorId: monitorId as string })); @@ -123,6 +127,7 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => { basePath, { to: dateRangeEnd, from: dateRangeStart }, false, + false, error as Error ); } diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx index 82b4006246ec..1fc4093a67d8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx @@ -22,6 +22,14 @@ export const JOB_CREATED_SUCCESS_MESSAGE = i18n.translate( } ); +export const JOB_CREATED_LAZY_SUCCESS_MESSAGE = i18n.translate( + 'xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreatedLazyNotificationText', + { + defaultMessage: + 'The analysis is waiting for an ML node to become available. It might take a while before results are added to the response times graph.', + } +); + export const JOB_CREATION_FAILED = i18n.translate( 'xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle', { diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index cbfba4ffcb23..35eab80c1596 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -51,16 +51,19 @@ export const MonitorDuration: React.FC = ({ monitorId }) => { const exploratoryViewLink = createExploratoryViewUrl( { - [`monitor-duration`]: { - reportType: 'kpi-over-time', - time: { from: dateRangeStart, to: dateRangeEnd }, - reportDefinitions: { - 'monitor.id': [monitorId] as string[], + reportType: 'kpi-over-time', + allSeries: [ + { + name: `${monitorId}-response-duration`, + time: { from: dateRangeStart, to: dateRangeEnd }, + reportDefinitions: { + 'monitor.id': [monitorId] as string[], + }, + breakdown: 'observer.geo.name', + operationType: 'average', + dataType: 'synthetics', }, - breakdown: 'observer.geo.name', - operationType: 'average', - dataType: 'synthetics', - }, + ], }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__snapshots__/availability_reporting.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__snapshots__/availability_reporting.test.tsx.snap deleted file mode 100644 index 5a660c7cb764..000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__snapshots__/availability_reporting.test.tsx.snap +++ /dev/null @@ -1,368 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AvailabilityReporting component renders correctly against snapshot 1`] = ` -Array [ - @media (max-width:1042px) { - -} - -

, - .c0 { - white-space: nowrap; - display: inline-block; -} - -@media (max-width:1042px) { - .c0 { - display: inline-block; - margin-right: 16px; - } -} - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - Location - - - - - - Availability - - - - - - Last check - - -
-
- Location -
-
-
- - - - au-heartbeat - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - 36m ago - -
-
-
- Location -
-
-
- - - - nyc-heartbeat - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - 36m ago - -
-
-
- Location -
-
-
- - - - spa-heartbeat - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - 36m ago - -
-
-
-
, -] -`; - -exports[`AvailabilityReporting component shallow renders correctly against snapshot 1`] = ` - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__snapshots__/location_status_tags.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__snapshots__/location_status_tags.test.tsx.snap deleted file mode 100644 index c30469eab3c3..000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__snapshots__/location_status_tags.test.tsx.snap +++ /dev/null @@ -1,1060 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationStatusTags component renders properly against props 1`] = ` - - - - - -`; - -exports[`LocationStatusTags component renders when all locations are down 1`] = ` -.c1 { - white-space: nowrap; - display: inline-block; -} - -.c0 { - max-height: 246px; - overflow: hidden; -} - -@media (max-width:1042px) { - .c1 { - display: inline-block; - margin-right: 16px; - } -} - -
-
-
-
- - - - - - - - - - - - - - - - - - - - -
-
- - - Location - - - - - - Availability - - - - - - Last check - - -
-
- Location -
-
-
- - - - Berlin - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - Sept 4, 2020 9:31:38 AM - -
-
-
- Location -
-
-
- - - - Islamabad - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - Sept 4, 2020 9:31:38 AM - -
-
-
-
-
-`; - -exports[`LocationStatusTags component renders when all locations are up 1`] = ` -.c1 { - white-space: nowrap; - display: inline-block; -} - -.c0 { - max-height: 246px; - overflow: hidden; -} - -@media (max-width:1042px) { - .c1 { - display: inline-block; - margin-right: 16px; - } -} - -
-
-
-
- - - - - - - - - - - - - - - - - - - - -
-
- - - Location - - - - - - Availability - - - - - - Last check - - -
-
- Location -
-
-
- - - - Berlin - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - Sept 4, 2020 9:31:38 AM - -
-
-
- Location -
-
-
- - - - Islamabad - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - Sept 4, 2020 9:31:38 AM - -
-
-
-
-
-`; - -exports[`LocationStatusTags component renders when there are many location 1`] = ` -.c1 { - white-space: nowrap; - display: inline-block; -} - -.c0 { - max-height: 246px; - overflow: hidden; -} - -@media (max-width:1042px) { - .c1 { - display: inline-block; - margin-right: 16px; - } -} - -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - Location - - - - - - Availability - - - - - - Last check - - -
-
- Location -
-
-
- - - - Berlin - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - Sept 4, 2020 9:31:38 AM - -
-
-
- Location -
-
-
- - - - Islamabad - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - Sept 4, 2020 9:31:38 AM - -
-
-
- Location -
-
-
- - - - New York - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - Sept 4, 2020 9:31:38 AM - -
-
-
- Location -
-
-
- - - - Paris - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - Sept 4, 2020 9:31:38 AM - -
-
-
- Location -
-
-
- - - - Sydney - - - -
-
-
-
- Availability -
-
- - 100.00 % - -
-
-
- Last check -
-
- - Sept 4, 2020 9:31:38 AM - -
-
-
-
-
-
-
-
- -
-
-
-
-
-`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.test.tsx index 3b1f15dc46f7..f62a308daa6d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.test.tsx @@ -6,15 +6,9 @@ */ import React from 'react'; -import { renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { AvailabilityReporting } from './availability_reporting'; import { StatusTag } from './location_status_tags'; - -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { - return { - htmlIdGenerator: () => () => `generated-id`, - }; -}); +import { render } from '../../../../lib/helper/rtl_helpers'; describe('AvailabilityReporting component', () => { let allLocations: StatusTag[]; @@ -45,13 +39,10 @@ describe('AvailabilityReporting component', () => { ]; }); - it('shallow renders correctly against snapshot', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); + it('renders correctly against snapshot', async () => { + const { findByText } = render(); - it('renders correctly against snapshot', () => { - const component = renderWithIntl(); - expect(component).toMatchSnapshot(); + expect(await findByText('This table contains 3 rows.')).toBeInTheDocument(); + expect(await findByText('au-heartbeat')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx index c17d2dd97d32..878752ef3ede 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx @@ -6,9 +6,8 @@ */ import React, { useState } from 'react'; -import { EuiBasicTable, EuiSpacer } from '@elastic/eui'; +import { EuiBasicTable, EuiSpacer, Criteria, Pagination } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Pagination } from '@elastic/eui/src/components/basic_table/pagination_bar'; import { StatusTag } from './location_status_tags'; import { TagLabel } from './tag_label'; import { AvailabilityLabel, LastCheckLabel, LocationLabel } from '../translations'; @@ -66,8 +65,8 @@ export const AvailabilityReporting: React.FC = ({ allLocations }) => { hidePerPageOptions: true, }; - const onTableChange = ({ page }: any) => { - setPageIndex(page.index); + const onTableChange = ({ page }: Criteria) => { + setPageIndex(page?.index ?? 0); }; const paginationProps = allLocations.length > pageSize ? { pagination } : {}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.test.tsx index 6b3cc2346583..3db2efd09808 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.test.tsx @@ -6,10 +6,10 @@ */ import React from 'react'; -import { renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { MonitorLocation } from '../../../../../common/runtime_types/monitor'; import { LocationStatusTags } from './index'; import { mockMoment } from '../../../../lib/helper/test_helpers'; +import { render } from '../../../../lib/helper/rtl_helpers'; mockMoment(); @@ -22,7 +22,7 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { describe('LocationStatusTags component', () => { let monitorLocations: MonitorLocation[]; - it('renders properly against props', () => { + it('renders properly against props', async () => { monitorLocations = [ { summary: { up: 4, down: 0 }, @@ -36,21 +36,21 @@ describe('LocationStatusTags component', () => { geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, - down_history: 0, + down_history: 2, }, { summary: { up: 0, down: 2 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, - down_history: 0, + down_history: 1, }, ]; - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); + const { findByText } = render(); + expect(await findByText('100.00 %')).toBeInTheDocument(); }); - it('renders when there are many location', () => { + it('renders when there are many location', async () => { monitorLocations = [ { summary: { up: 0, down: 1 }, @@ -64,28 +64,28 @@ describe('LocationStatusTags component', () => { geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, - down_history: 0, + down_history: 3, }, { summary: { up: 0, down: 1 }, geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, - down_history: 0, + down_history: 2, }, { summary: { up: 0, down: 1 }, geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, - down_history: 0, + down_history: 1, }, { summary: { up: 0, down: 1 }, geo: { name: 'New York', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, - down_history: 0, + down_history: 4, }, { summary: { up: 0, down: 1 }, @@ -109,11 +109,11 @@ describe('LocationStatusTags component', () => { down_history: 0, }, ]; - const component = renderWithIntl(); - expect(component).toMatchSnapshot(); + const { findAllByText } = render(); + expect(await findAllByText('100.00 %')).toHaveLength(3); }); - it('renders when all locations are up', () => { + it('renders when all locations are up', async () => { monitorLocations = [ { summary: { up: 4, down: 0 }, @@ -130,28 +130,28 @@ describe('LocationStatusTags component', () => { down_history: 0, }, ]; - const component = renderWithIntl(); - expect(component).toMatchSnapshot(); + const { findAllByText } = render(); + expect(await findAllByText('100.00 %')).toHaveLength(2); }); - it('renders when all locations are down', () => { + it('renders when all locations are down', async () => { monitorLocations = [ { summary: { up: 0, down: 2 }, geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: 'Oct 26, 2020 7:49:20 AM', - up_history: 4, - down_history: 0, + up_history: 0, + down_history: 2, }, { summary: { up: 0, down: 2 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: 'Oct 26, 2020 7:49:20 AM', - up_history: 4, - down_history: 0, + up_history: 0, + down_history: 2, }, ]; - const component = renderWithIntl(); - expect(component).toMatchSnapshot(); + const { findAllByText } = render(); + expect(await findAllByText('0.00 %')).toHaveLength(2); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.test.tsx new file mode 100644 index 000000000000..8b23d867572f --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { + BROWSER_TRACE_NAME, + BROWSER_TRACE_START, + BROWSER_TRACE_TYPE, + useStepWaterfallMetrics, +} from './use_step_waterfall_metrics'; +import * as reduxHooks from 'react-redux'; +import * as searchHooks from '../../../../../../observability/public/hooks/use_es_search'; + +describe('useStepWaterfallMetrics', () => { + jest + .spyOn(reduxHooks, 'useSelector') + .mockReturnValue({ settings: { heartbeatIndices: 'heartbeat-*' } }); + + it('returns result as expected', () => { + // @ts-ignore + const searchHook = jest.spyOn(searchHooks, 'useEsSearch').mockReturnValue({ + loading: false, + data: { + hits: { + total: { value: 2, relation: 'eq' }, + hits: [ + { + fields: { + [BROWSER_TRACE_TYPE]: ['mark'], + [BROWSER_TRACE_NAME]: ['navigationStart'], + [BROWSER_TRACE_START]: [3456789], + }, + }, + { + fields: { + [BROWSER_TRACE_TYPE]: ['mark'], + [BROWSER_TRACE_NAME]: ['domContentLoaded'], + [BROWSER_TRACE_START]: [4456789], + }, + }, + ], + }, + } as any, + }); + + const { result } = renderHook( + (props) => + useStepWaterfallMetrics({ + checkGroup: '44D-444FFF-444-FFF-3333', + hasNavigationRequest: true, + stepIndex: 1, + }), + {} + ); + + expect(searchHook).toHaveBeenCalledWith( + { + body: { + _source: false, + fields: ['browser.*'], + query: { + bool: { + filter: [ + { + term: { + 'synthetics.step.index': 1, + }, + }, + { + term: { + 'monitor.check_group': '44D-444FFF-444-FFF-3333', + }, + }, + { + term: { + 'synthetics.type': 'step/metrics', + }, + }, + ], + }, + }, + size: 1000, + }, + index: 'heartbeat-*', + }, + ['heartbeat-*', '44D-444FFF-444-FFF-3333', true] + ); + expect(result.current).toEqual({ + loading: false, + metrics: [ + { + id: 'domContentLoaded', + offset: 1000, + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.ts new file mode 100644 index 000000000000..cf60f6d7d556 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_step_waterfall_metrics.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; +import { createEsParams, useEsSearch } from '../../../../../../observability/public'; +import { selectDynamicSettings } from '../../../../state/selectors'; +import { MarkerItems } from '../waterfall/context/waterfall_chart'; + +export interface Props { + checkGroup: string; + stepIndex: number; + hasNavigationRequest?: boolean; +} +export const BROWSER_TRACE_TYPE = 'browser.relative_trace.type'; +export const BROWSER_TRACE_NAME = 'browser.relative_trace.name'; +export const BROWSER_TRACE_START = 'browser.relative_trace.start.us'; +export const NAVIGATION_START = 'navigationStart'; + +export const useStepWaterfallMetrics = ({ checkGroup, hasNavigationRequest, stepIndex }: Props) => { + const { settings } = useSelector(selectDynamicSettings); + + const heartbeatIndices = settings?.heartbeatIndices || ''; + + const { data, loading } = useEsSearch( + hasNavigationRequest + ? createEsParams({ + index: heartbeatIndices!, + body: { + query: { + bool: { + filter: [ + { + term: { + 'synthetics.step.index': stepIndex, + }, + }, + { + term: { + 'monitor.check_group': checkGroup, + }, + }, + { + term: { + 'synthetics.type': 'step/metrics', + }, + }, + ], + }, + }, + fields: ['browser.*'], + size: 1000, + _source: false, + }, + }) + : {}, + [heartbeatIndices, checkGroup, hasNavigationRequest] + ); + + if (!hasNavigationRequest) { + return { metrics: [], loading: false }; + } + + const metrics: MarkerItems = []; + + if (data && hasNavigationRequest) { + const metricDocs = data.hits.hits as unknown as Array<{ fields: any }>; + let navigationStart = 0; + let navigationStartExist = false; + + metricDocs.forEach(({ fields }) => { + if (fields[BROWSER_TRACE_TYPE]?.[0] === 'mark') { + const { [BROWSER_TRACE_NAME]: metricType, [BROWSER_TRACE_START]: metricValue } = fields; + if (metricType?.[0] === NAVIGATION_START) { + navigationStart = metricValue?.[0]; + navigationStartExist = true; + } + } + }); + + if (navigationStartExist) { + metricDocs.forEach(({ fields }) => { + if (fields[BROWSER_TRACE_TYPE]?.[0] === 'mark') { + const { [BROWSER_TRACE_NAME]: metricType, [BROWSER_TRACE_START]: metricValue } = fields; + if (metricType?.[0] !== NAVIGATION_START) { + metrics.push({ + id: metricType?.[0], + offset: (metricValue?.[0] - navigationStart) / 1000, + }); + } + } + }); + } + } + + return { metrics, loading }; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx index 044353125e74..d249c23c44d7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx @@ -14,6 +14,7 @@ import { getNetworkEvents } from '../../../../../state/actions/network_events'; import { networkEventsSelector } from '../../../../../state/selectors'; import { WaterfallChartWrapper } from './waterfall_chart_wrapper'; import { extractItems } from './data_formatting'; +import { useStepWaterfallMetrics } from '../use_step_waterfall_metrics'; export const NO_DATA_TEXT = i18n.translate('xpack.uptime.synthetics.stepDetail.waterfallNoData', { defaultMessage: 'No waterfall data could be found for this step', @@ -44,6 +45,12 @@ export const WaterfallChartContainer: React.FC = ({ checkGroup, stepIndex const isWaterfallSupported = networkEvents?.isWaterfallSupported; const hasEvents = networkEvents?.events?.length > 0; + const { metrics } = useStepWaterfallMetrics({ + checkGroup, + stepIndex, + hasNavigationRequest: networkEvents?.hasNavigationRequest, + }); + return ( <> {!waterfallLoaded && ( @@ -70,6 +77,7 @@ export const WaterfallChartContainer: React.FC = ({ checkGroup, stepIndex {waterfallLoaded && hasEvents && isWaterfallSupported && ( )} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx index fbb6f2c75a54..81ed2d024340 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx @@ -31,7 +31,11 @@ describe('WaterfallChartWrapper', () => { it('renders the correct sidebar items', () => { const { getAllByTestId } = render( - + ); const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index 26be3cbadee4..8071fd1e3c4d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -14,6 +14,7 @@ import { useTrackMetric, METRIC_TYPE } from '../../../../../../../observability/ import { WaterfallFilter } from './waterfall_filter'; import { WaterfallFlyout } from './waterfall_flyout'; import { WaterfallSidebarItem } from './waterfall_sidebar_item'; +import { MarkerItems } from '../../waterfall/context/waterfall_chart'; export const renderLegendItem: RenderItem = (item) => { return ( @@ -26,9 +27,10 @@ export const renderLegendItem: RenderItem = (item) => { interface Props { total: number; data: NetworkItems; + markerItems?: MarkerItems; } -export const WaterfallChartWrapper: React.FC = ({ data, total }) => { +export const WaterfallChartWrapper: React.FC = ({ data, total, markerItems }) => { const [query, setQuery] = useState(''); const [activeFilters, setActiveFilters] = useState([]); const [onlyHighlighted, setOnlyHighlighted] = useState(false); @@ -107,6 +109,7 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { return ( props.theme.eui.euiZLevel4}; height: 100%; + &&& { + .echAnnotation__icon { + top: 8px; + } + } `; interface WaterfallChartSidebarContainer { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx index 8723dd744132..d4a7cf6a1f66 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx @@ -24,6 +24,7 @@ import { WaterfallChartChartContainer, WaterfallChartTooltip } from './styles'; import { useWaterfallContext, WaterfallData } from '..'; import { WaterfallTooltipContent } from './waterfall_tooltip_content'; import { formatTooltipHeading } from '../../step_detail/waterfall/data_formatting'; +import { WaterfallChartMarkers } from './waterfall_markers'; const getChartHeight = (data: WaterfallData): number => { // We get the last item x(number of bars) and adds 1 to cater for 0 index @@ -120,6 +121,7 @@ export const WaterfallBarChart = ({ styleAccessor={barStyleAccessor} data={chartData} /> + ); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx index 3a7ab421b627..3824b9ae19d0 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/charts'; import { useChartTheme } from '../../../../../hooks/use_chart_theme'; import { WaterfallChartFixedAxisContainer } from './styles'; +import { WaterfallChartMarkers } from './waterfall_markers'; interface Props { tickFormat: TickFormatter; @@ -59,6 +60,7 @@ export const WaterfallChartFixedAxis = ({ tickFormat, domain, barStyleAccessor } styleAccessor={barStyleAccessor} data={[{ x: 0, y0: 0, y1: 1 }]} /> + ); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_markers.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_markers.tsx new file mode 100644 index 000000000000..b341b052e010 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_markers.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AnnotationDomainType, LineAnnotation } from '@elastic/charts'; +import { EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useWaterfallContext } from '..'; +import { useTheme } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; + +export const FCP_LABEL = i18n.translate('xpack.uptime.synthetics.waterfall.fcpLabel', { + defaultMessage: 'First contentful paint', +}); + +export const LCP_LABEL = i18n.translate('xpack.uptime.synthetics.waterfall.lcpLabel', { + defaultMessage: 'Largest contentful paint', +}); + +export const LAYOUT_SHIFT_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.layoutShiftLabel', + { + defaultMessage: 'Layout shift', + } +); + +export const LOAD_EVENT_LABEL = i18n.translate('xpack.uptime.synthetics.waterfall.loadEventLabel', { + defaultMessage: 'Load event', +}); + +export const DOCUMENT_CONTENT_LOADED_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.domContentLabel', + { + defaultMessage: 'DOM Content Loaded', + } +); + +export function WaterfallChartMarkers() { + const { markerItems } = useWaterfallContext(); + + const theme = useTheme(); + + if (!markerItems) { + return null; + } + + const markersInfo: Record = { + domContentLoaded: { label: DOCUMENT_CONTENT_LOADED_LABEL, color: theme.eui.euiColorVis0 }, + firstContentfulPaint: { label: FCP_LABEL, color: theme.eui.euiColorVis1 }, + largestContentfulPaint: { label: LCP_LABEL, color: theme.eui.euiColorVis2 }, + layoutShift: { label: LAYOUT_SHIFT_LABEL, color: theme.eui.euiColorVis3 }, + loadEvent: { label: LOAD_EVENT_LABEL, color: theme.eui.euiColorVis9 }, + }; + + return ( + + {markerItems.map(({ id, offset }) => ( + } + style={{ + line: { + strokeWidth: 2, + stroke: markersInfo[id]?.color ?? theme.eui.euiColorMediumShade, + opacity: 1, + }, + }} + /> + ))} + + ); +} + +const Wrapper = euiStyled.span` + &&& { + > .echAnnotation__icon { + top: 8px; + } + } +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx index 16b3a24de7d0..cce0533293e0 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx @@ -10,6 +10,17 @@ import { WaterfallData, WaterfallDataEntry, WaterfallMetadata } from '../types'; import { OnSidebarClick, OnElementClick, OnProjectionClick } from '../components/use_flyout'; import { SidebarItems } from '../../step_detail/waterfall/types'; +export type MarkerItems = Array<{ + id: + | 'domContentLoaded' + | 'firstContentfulPaint' + | 'largestContentfulPaint' + | 'layoutShift' + | 'loadEvent' + | 'navigationStart'; + offset: number; +}>; + export interface IWaterfallContext { totalNetworkRequests: number; highlightedNetworkRequests: number; @@ -26,6 +37,7 @@ export interface IWaterfallContext { item: WaterfallDataEntry['config']['tooltipProps'], index?: number ) => JSX.Element; + markerItems?: MarkerItems; } export const WaterfallContext = createContext>({}); @@ -43,11 +55,13 @@ interface ProviderProps { legendItems?: IWaterfallContext['legendItems']; metadata: IWaterfallContext['metadata']; renderTooltipItem: IWaterfallContext['renderTooltipItem']; + markerItems?: MarkerItems; } export const WaterfallProvider: React.FC = ({ children, data, + markerItems, onElementClick, onProjectionClick, onSidebarClick, @@ -64,6 +78,7 @@ export const WaterfallProvider: React.FC = ({ { - const { loading, error } = useSelector(indexStatusSelector); + const { loading, error, data } = useSelector(indexStatusSelector); const { lastRefresh } = useContext(UptimeRefreshContext); const { settings } = useSelector(selectDynamicSettings); @@ -29,6 +29,7 @@ export const useHasData = () => { }, [dispatch]); return { + data, error, loading, settings, diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap deleted file mode 100644 index e6e7250dd553..000000000000 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap +++ /dev/null @@ -1,1917 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MonitorList component MonitorListPagination component renders a no items message when no data is provided 1`] = ` - - - - - -`; - -exports[`MonitorList component MonitorListPagination component renders the pagination 1`] = ` - - - - - -`; - -exports[`MonitorList component renders a no items message when no data is provided 1`] = ` - - - - - -`; - -exports[`MonitorList component renders error list 1`] = ` - - - - - -`; - -exports[`MonitorList component renders loading state 1`] = ` - - - - - -`; - -exports[`MonitorList component renders the monitor list 1`] = ` -.c2 { - padding-right: 4px; -} - -.c3 { - margin-top: 12px; -} - -.c0 { - position: relative; -} - -@media (max-width:574px) { - .c1 { - min-width: 230px; - } -} - -
-
-
-
- Monitors -
-
-
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - Status - - - - - - Name - - - - - - Url - - - - - - Tags - - - - - - TLS Certificate - - - - - - Downtime history - - - - - - Status alert - - - - - - -
-
- Status -
-
-
-
-
- - - -
-
-
-
- -
-
- in 0/1 location, -
-
-
- -
-
- Checked Sept 4, 2020 9:31:38 AM -
-
-
-
-
-
-
-
- Name -
-
-
- -
- -
-
-
-
-
- Url -
-
-
-
-
- Tags -
-
-
-
- TLS Certificate -
-
- - -- - -
-
-
- -
-
- -- -
-
-
-
-
-
- Status alert -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
- Status -
-
-
-
-
- - - -
-
-
-
- -
-
- in 0/1 location, -
-
-
- -
-
- Checked Sept 4, 2020 9:31:38 AM -
-
-
-
-
-
-
-
- Name -
-
-
- -
- -
-
-
-
-
- Url -
-
-
-
-
- Tags -
-
-
-
- TLS Certificate -
-
- - -- - -
-
-
- -
-
- -- -
-
-
-
-
-
- Status alert -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
- -
-
- -
-
-
-
-
-`; - -exports[`MonitorList component shallow renders the monitor list 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.test.tsx index 3a32d8c943af..703e2653ff0a 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.test.tsx @@ -16,13 +16,12 @@ import { MonitorSummary, } from '../../../../common/runtime_types'; import { MonitorListComponent, noItemsMessage } from './monitor_list'; -import { renderWithRouter, shallowWithRouter } from '../../../lib'; import * as redux from 'react-redux'; import moment from 'moment'; import { IHttpFetchError } from '../../../../../../../src/core/public'; import { mockMoment } from '../../../lib/helper/test_helpers'; import { render } from '../../../lib/helper/rtl_helpers'; -import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import { NO_DATA_MESSAGE } from './translations'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { @@ -150,20 +149,8 @@ describe('MonitorList component', () => { global.localStorage = localStorageMock; }); - it('shallow renders the monitor list', () => { - const component = shallowWithRouter( - - ); - - expect(component).toMatchSnapshot(); - }); - - it('renders a no items message when no data is provided', () => { - const component = shallowWithRouter( + it('renders a no items message when no data is provided', async () => { + const { findByText } = render( { nextPagePagination: null, prevPagePagination: null, }, - loading: true, + loading: false, }} pageSize={10} setPageSize={jest.fn()} /> ); - expect(component).toMatchSnapshot(); - }); - - it('renders the monitor list', () => { - const component = renderWithRouter( - - - - ); - - expect(component).toMatchSnapshot(); + expect(await findByText(NO_DATA_MESSAGE)).toBeInTheDocument(); }); - it('renders error list', () => { - const component = shallowWithRouter( + it('renders the monitor list', async () => { + const { findByLabelText } = render( { /> ); - expect(component).toMatchSnapshot(); + expect( + await findByLabelText( + 'Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying 2 items.' + ) + ).toBeInTheDocument(); }); - it('renders loading state', () => { - const component = shallowWithRouter( + it('renders error list', async () => { + const { findByText } = render( ); - expect(component).toMatchSnapshot(); + expect(await findByText('foo message')).toBeInTheDocument(); }); describe('MonitorListPagination component', () => { @@ -244,8 +221,8 @@ describe('MonitorList component', () => { }; }); - it('renders the pagination', () => { - const component = shallowWithRouter( + it('renders the pagination', async () => { + const { findByText, findByLabelText } = render( { /> ); - expect(component).toMatchSnapshot(); - }); - - it('renders a no items message when no data is provided', () => { - const component = shallowWithRouter( - - ); - - expect(component).toMatchSnapshot(); + expect(await findByText('Rows per page: 10')).toBeInTheDocument(); + expect(await findByLabelText('Prev page of results')).toBeInTheDocument(); + expect(await findByLabelText('Next page of results')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx index 580160bac401..8171f7e19865 100644 --- a/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx @@ -9,7 +9,7 @@ import React, { createContext, useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useFetcher } from '../../../observability/public'; import { DataPublicPluginStart, IndexPattern } from '../../../../../src/plugins/data/public'; -import { selectDynamicSettings } from '../state/selectors'; +import { indexStatusSelector, selectDynamicSettings } from '../state/selectors'; import { getDynamicSettings } from '../state/actions/dynamic_settings'; export const UptimeIndexPatternContext = createContext({} as IndexPattern); @@ -19,6 +19,8 @@ export const UptimeIndexPatternContextProvider: React.FC<{ data: DataPublicPlugi data: { indexPatterns }, }) => { const { settings } = useSelector(selectDynamicSettings); + const { data: indexStatus } = useSelector(indexStatusSelector); + const dispatch = useDispatch(); useEffect(() => { @@ -30,11 +32,11 @@ export const UptimeIndexPatternContextProvider: React.FC<{ data: DataPublicPlugi const heartbeatIndices = settings?.heartbeatIndices || ''; const { data } = useFetcher>(async () => { - if (heartbeatIndices) { + if (heartbeatIndices && indexStatus?.indexExists) { // this only creates an index pattern in memory, not as saved object return indexPatterns.create({ title: heartbeatIndices }); } - }, [heartbeatIndices]); + }, [heartbeatIndices, indexStatus?.indexExists]); return ; }; diff --git a/x-pack/plugins/uptime/public/hooks/use_composite_image.test.tsx b/x-pack/plugins/uptime/public/hooks/use_composite_image.test.tsx index 79e0cde1eaab..9e2cb1e498b7 100644 --- a/x-pack/plugins/uptime/public/hooks/use_composite_image.test.tsx +++ b/x-pack/plugins/uptime/public/hooks/use_composite_image.test.tsx @@ -191,10 +191,13 @@ describe('use composite image', () => { expect(composeSpy.mock.calls[0][1]).toBe(canvasMock); expect(composeSpy.mock.calls[0][2]).toBe(blocks); - await waitFor(() => { - expect(onComposeImageSuccess).toHaveBeenCalledTimes(1); - expect(onComposeImageSuccess).toHaveBeenCalledWith('compose success'); - }); + await waitFor( + () => { + expect(onComposeImageSuccess).toHaveBeenCalledTimes(1); + expect(onComposeImageSuccess).toHaveBeenCalledWith('compose success'); + }, + { timeout: 10000 } + ); }); }); }); diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index 46f77b547779..ac129bdb327d 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -29,6 +29,7 @@ import { stringifyUrlParams } from './stringify_url_params'; import { ClientPluginsStart } from '../../apps/plugin'; import { triggersActionsUiMock } from '../../../../triggers_actions_ui/public/mocks'; import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { UptimeRefreshContextProvider, UptimeStartupPluginsContextProvider } from '../../contexts'; interface KibanaProps { services?: KibanaServices; @@ -129,9 +130,13 @@ export function MockKibanaProvider({ }; return ( - - {children} - + + + + {children} + + + ); } diff --git a/x-pack/plugins/uptime/public/pages/__snapshots__/certificates.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__snapshots__/certificates.test.tsx.snap deleted file mode 100644 index e1009236ef4b..000000000000 --- a/x-pack/plugins/uptime/public/pages/__snapshots__/certificates.test.tsx.snap +++ /dev/null @@ -1,91 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CertificatesPage shallow renders expected elements for valid props 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/uptime/public/pages/__snapshots__/monitor.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__snapshots__/monitor.test.tsx.snap deleted file mode 100644 index cd92334cf72f..000000000000 --- a/x-pack/plugins/uptime/public/pages/__snapshots__/monitor.test.tsx.snap +++ /dev/null @@ -1,91 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MonitorPage shallow renders expected elements for valid props 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/uptime/public/pages/__snapshots__/not_found.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__snapshots__/not_found.test.tsx.snap deleted file mode 100644 index df67e320d7aa..000000000000 --- a/x-pack/plugins/uptime/public/pages/__snapshots__/not_found.test.tsx.snap +++ /dev/null @@ -1,91 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NotFoundPage render component for valid props 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/uptime/public/pages/__snapshots__/overview.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__snapshots__/overview.test.tsx.snap deleted file mode 100644 index 3e532d0d8e78..000000000000 --- a/x-pack/plugins/uptime/public/pages/__snapshots__/overview.test.tsx.snap +++ /dev/null @@ -1,91 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MonitorPage shallow renders expected elements for valid props 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/uptime/public/pages/certificates.test.tsx b/x-pack/plugins/uptime/public/pages/certificates.test.tsx index ff5f1afcaa29..d61fde13663a 100644 --- a/x-pack/plugins/uptime/public/pages/certificates.test.tsx +++ b/x-pack/plugins/uptime/public/pages/certificates.test.tsx @@ -6,11 +6,18 @@ */ import React from 'react'; -import { shallowWithRouter } from '../lib'; import { CertificatesPage } from './certificates'; +import { render } from '../lib/helper/rtl_helpers'; describe('CertificatesPage', () => { - it('shallow renders expected elements for valid props', () => { - expect(shallowWithRouter()).toMatchSnapshot(); + it('renders expected elements for valid props', async () => { + const { findByText } = render(); + + expect(await findByText('This table contains 0 rows; Page 1 of 0.')).toBeInTheDocument(); + expect( + await findByText( + 'No Certificates found. Note: Certificates are only visible for Heartbeat 7.8+' + ) + ).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/pages/monitor.test.tsx b/x-pack/plugins/uptime/public/pages/monitor.test.tsx index 80fcfcc27196..053121d505de 100644 --- a/x-pack/plugins/uptime/public/pages/monitor.test.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor.test.tsx @@ -7,10 +7,19 @@ import React from 'react'; import { MonitorPage } from './monitor'; -import { shallowWithRouter } from '../lib'; +import { render } from '../lib/helper/rtl_helpers'; describe('MonitorPage', () => { - it('shallow renders expected elements for valid props', () => { - expect(shallowWithRouter()).toMatchSnapshot(); + it('renders', async () => { + const { findByText } = render(); + + expect(await findByText('Up in 0 location')).toBeInTheDocument(); + expect(await findByText('Overall availability')).toBeInTheDocument(); + expect(await findByText('0.00 %')).toBeInTheDocument(); + expect(await findByText('Url')).toBeInTheDocument(); + expect(await findByText('Monitor ID')).toBeInTheDocument(); + expect(await findByText('Tags')).toBeInTheDocument(); + expect(await findByText('Set tags')).toBeInTheDocument(); + expect(await findByText('Monitoring from')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/pages/not_found.test.tsx b/x-pack/plugins/uptime/public/pages/not_found.test.tsx index 8d5b20e45303..23f753478417 100644 --- a/x-pack/plugins/uptime/public/pages/not_found.test.tsx +++ b/x-pack/plugins/uptime/public/pages/not_found.test.tsx @@ -6,12 +6,14 @@ */ import React from 'react'; -import { shallowWithRouter } from '../lib'; import { NotFoundPage } from './not_found'; +import { render } from '../lib/helper/rtl_helpers'; describe('NotFoundPage', () => { - it('render component for valid props', () => { - const component = shallowWithRouter(); - expect(component).toMatchSnapshot(); + it('render component', async () => { + const { findByText } = render(); + + expect(await findByText('Page not found')).toBeInTheDocument(); + expect(await findByText('Back to home')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/pages/overview.test.tsx b/x-pack/plugins/uptime/public/pages/overview.test.tsx index f827cf66b034..b3aa4714fa66 100644 --- a/x-pack/plugins/uptime/public/pages/overview.test.tsx +++ b/x-pack/plugins/uptime/public/pages/overview.test.tsx @@ -7,10 +7,16 @@ import React from 'react'; import { OverviewPageComponent } from './overview'; -import { shallowWithRouter } from '../lib'; +import { render } from '../lib/helper/rtl_helpers'; describe('MonitorPage', () => { - it('shallow renders expected elements for valid props', () => { - expect(shallowWithRouter()).toMatchSnapshot(); + it('renders expected elements for valid props', async () => { + const { findByText, findByPlaceholderText } = render(); + + expect(await findByText('No uptime monitors found')).toBeInTheDocument(); + + expect( + await findByPlaceholderText('Search by monitor ID, name, or url (E.g. http:// )') + ).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx b/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx index fecb6bd1e845..93df71698057 100644 --- a/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx +++ b/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx @@ -135,12 +135,18 @@ export const StepDetailPageChildren = () => { /> ); }; +import { getDynamicSettings } from '../../state/actions/dynamic_settings'; export const StepDetailPage: React.FC = () => { useInitApp(); const { checkGroupId, stepIndex } = useParams<{ checkGroupId: string; stepIndex: string }>(); useTrackPageview({ app: 'uptime', path: 'stepDetail' }); useTrackPageview({ app: 'uptime', path: 'stepDetail', delay: 15000 }); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(getDynamicSettings()); + }, [dispatch]); return ; }; diff --git a/x-pack/plugins/uptime/public/state/actions/types.ts b/x-pack/plugins/uptime/public/state/actions/types.ts index b830f8162404..754710db306e 100644 --- a/x-pack/plugins/uptime/public/state/actions/types.ts +++ b/x-pack/plugins/uptime/public/state/actions/types.ts @@ -48,6 +48,7 @@ export interface MonitorDetailsActionPayload { export interface CreateMLJobSuccess { count: number; jobId: string; + awaitingNodeAssignment: boolean; } export interface DeleteJobResults { diff --git a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts index 95784467610f..24f2d667323d 100644 --- a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts +++ b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts @@ -57,10 +57,12 @@ export const createMLJob = async ({ const response: DataRecognizerConfigResponse = await apiService.post(url, data); if (response?.jobs?.[0]?.id === getMLJobId(monitorId)) { const jobResponse = response.jobs[0]; + const datafeedResponse = response.datafeeds[0]; if (jobResponse.success) { return { count: 1, jobId: jobResponse.id, + awaitingNodeAssignment: datafeedResponse.awaitingMlNodeAllocation === true, }; } else { const { error } = jobResponse; diff --git a/x-pack/plugins/uptime/public/state/reducers/network_events.ts b/x-pack/plugins/uptime/public/state/reducers/network_events.ts index 56fef5947fb0..e58a8f75c7fa 100644 --- a/x-pack/plugins/uptime/public/state/reducers/network_events.ts +++ b/x-pack/plugins/uptime/public/state/reducers/network_events.ts @@ -23,6 +23,7 @@ export interface NetworkEventsState { loading: boolean; error?: Error; isWaterfallSupported: boolean; + hasNavigationRequest?: boolean; }; }; } @@ -71,7 +72,14 @@ export const networkEventsReducer = handleActions( [String(getNetworkEventsSuccess)]: ( state: NetworkEventsState, { - payload: { events, total, checkGroup, stepIndex, isWaterfallSupported }, + payload: { + events, + total, + checkGroup, + stepIndex, + isWaterfallSupported, + hasNavigationRequest, + }, }: Action ) => { return { @@ -85,12 +93,14 @@ export const networkEventsReducer = handleActions( events, total, isWaterfallSupported, + hasNavigationRequest, } : { loading: false, events, total, isWaterfallSupported, + hasNavigationRequest, }, } : { @@ -99,6 +109,7 @@ export const networkEventsReducer = handleActions( events, total, isWaterfallSupported, + hasNavigationRequest, }, }, }; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts index b7f417168fd3..e0cd17327a9b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts @@ -298,6 +298,7 @@ describe('getNetworkEvents', () => { "url": "www.test.com", }, ], + "hasNavigationRequest": false, "isWaterfallSupported": true, "total": 1, } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index b27b1a4c736d..20e5c3a2a118 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -20,7 +20,12 @@ export const secondsToMillis = (seconds: number) => export const getNetworkEvents: UMElasticsearchQueryFn< GetNetworkEventsParams, - { events: NetworkEvent[]; total: number; isWaterfallSupported: boolean } + { + events: NetworkEvent[]; + total: number; + isWaterfallSupported: boolean; + hasNavigationRequest: boolean; + } > = async ({ uptimeEsClient, checkGroup, stepIndex }) => { const params = { track_total_hits: true, @@ -41,25 +46,34 @@ export const getNetworkEvents: UMElasticsearchQueryFn< const { body: result } = await uptimeEsClient.search({ body: params }); let isWaterfallSupported = false; + let hasNavigationRequest = false; + const events = result.hits.hits.map((event: any) => { - if (event._source.http && event._source.url) { + const docSource = event._source; + + if (docSource.http && docSource.url) { isWaterfallSupported = true; } - const requestSentTime = secondsToMillis(event._source.synthetics.payload.request_sent_time); - const loadEndTime = secondsToMillis(event._source.synthetics.payload.load_end_time); - const securityDetails = event._source.tls?.server?.x509; + const requestSentTime = secondsToMillis(docSource.synthetics.payload.request_sent_time); + const loadEndTime = secondsToMillis(docSource.synthetics.payload.load_end_time); + const securityDetails = docSource.tls?.server?.x509; + + if (docSource.synthetics.payload?.is_navigation_request) { + // if step has navigation request, this means we will display waterfall metrics in ui + hasNavigationRequest = true; + } return { - timestamp: event._source['@timestamp'], - method: event._source.http?.request?.method, - url: event._source.url?.full, - status: event._source.http?.response?.status, - mimeType: event._source.http?.response?.mime_type, + timestamp: docSource['@timestamp'], + method: docSource.http?.request?.method, + url: docSource.url?.full, + status: docSource.http?.response?.status, + mimeType: docSource.http?.response?.mime_type, requestSentTime, loadEndTime, - timings: event._source.synthetics.payload.timings, - transferSize: event._source.synthetics.payload.transfer_size, - resourceSize: event._source.synthetics.payload.resource_size, + timings: docSource.synthetics.payload.timings, + transferSize: docSource.synthetics.payload.transfer_size, + resourceSize: docSource.synthetics.payload.resource_size, certificates: securityDetails ? { issuer: securityDetails.issuer?.common_name, @@ -68,9 +82,9 @@ export const getNetworkEvents: UMElasticsearchQueryFn< validTo: securityDetails.not_after, } : undefined, - requestHeaders: event._source.http?.request?.headers, - responseHeaders: event._source.http?.response?.headers, - ip: event._source.http?.response?.remote_i_p_address, + requestHeaders: docSource.http?.request?.headers, + responseHeaders: docSource.http?.response?.headers, + ip: docSource.http?.response?.remote_i_p_address, }; }); @@ -78,5 +92,6 @@ export const getNetworkEvents: UMElasticsearchQueryFn< total: result.hits.total.value, events, isWaterfallSupported, + hasNavigationRequest, }; }; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index c98fe9c7d67f..e3a062a08ffb 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -257,5 +257,21 @@ export default function createGetTests({ getService }: FtrProviderContext) { }, ]); }); + + it('7.16.0 migrates security_solution (Legacy) siem.notifications with "ruleAlertId" to be saved object references', async () => { + // NOTE: We hae to use elastic search directly against the ".kibana" index because alerts do not expose the references which we want to test exists + const response = await es.get<{ references: [{}] }>({ + index: '.kibana', + id: 'alert:d7a8c6a1-9394-48df-a634-d5457c35d747', + }); + expect(response.statusCode).to.eql(200); + expect(response.body._source?.references).to.eql([ + { + name: 'param:alert_0', + id: '1a4ed6ae-3c89-44b2-999d-db554144504c', + type: 'alert', + }, + ]); + }); }); } diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 958c44df3575..762fc1642a87 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -37,7 +37,7 @@ export default function ({ getService }: FtrProviderContext) { securitySolutionCases: ['all', 'read'], infrastructure: ['all', 'read'], logs: ['all', 'read'], - apm: ['all', 'read', 'minimal_all', 'minimal_read', 'alerts_all', 'alerts_read'], + apm: ['all', 'read'], discover: [ 'all', 'read', diff --git a/x-pack/test/api_integration/apis/security_solution/timeline.ts b/x-pack/test/api_integration/apis/security_solution/timeline.ts index 10e082cf4400..f586719e6015 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline.ts @@ -16,44 +16,121 @@ import { createBasicTimeline, createBasicTimelineTemplate } from './saved_object export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); describe('Timeline', () => { - it('Make sure that we get Timeline data', async () => { - const titleToSaved = 'hello timeline'; - await createBasicTimeline(supertest, titleToSaved); + describe('timelines', () => { + it('Make sure that we get Timeline data', async () => { + const titleToSaved = 'hello timeline'; + await createBasicTimeline(supertest, titleToSaved); - const resp = await supertest.get('/api/timelines').set('kbn-xsrf', 'true'); + const resp = await supertest.get('/api/timelines').set('kbn-xsrf', 'true'); - const timelines = resp.body.timeline; + const timelines = resp.body.timeline; - expect(timelines.length).to.greaterThan(0); - }); + expect(timelines.length).to.greaterThan(0); + }); + + it('Make sure that pagination is working in Timeline query', async () => { + const titleToSaved = 'hello timeline'; + await createBasicTimeline(supertest, titleToSaved); + + const resp = await supertest + .get('/api/timelines?page_size=1&page_index=1') + .set('kbn-xsrf', 'true'); - it('Make sure that pagination is working in Timeline query', async () => { - const titleToSaved = 'hello timeline'; - await createBasicTimeline(supertest, titleToSaved); + const timelines = resp.body.timeline; - const resp = await supertest - .get('/api/timelines?page_size=1&page_index=1') - .set('kbn-xsrf', 'true'); + expect(timelines.length).to.equal(1); + }); - const timelines = resp.body.timeline; + it('Make sure that we get Timeline template data', async () => { + const titleToSaved = 'hello timeline template'; + await createBasicTimelineTemplate(supertest, titleToSaved); - expect(timelines.length).to.equal(1); + const resp = await supertest + .get('/api/timelines?timeline_type=template') + .set('kbn-xsrf', 'true'); + + const templates: SavedTimeline[] = resp.body.timeline; + + expect(templates.length).to.greaterThan(0); + expect(templates.filter((t) => t.timelineType === TimelineType.default).length).to.equal(0); + }); }); + describe('resolve timeline', () => { + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' + ); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' + ); + }); + + it('should return outcome exactMatch when the id is unchanged', async () => { + const resp = await supertest + .get('/api/timeline/resolve') + .query({ id: '8dc70950-1012-11ec-9ad3-2d7c6600c0f7' }); + + expect(resp.body.data.outcome).to.be('exactMatch'); + expect(resp.body.data.alias_target_id).to.be(undefined); + expect(resp.body.data.timeline.title).to.be('Awesome Timeline'); + }); + + describe('notes', () => { + it('should return notes with eventId', async () => { + const resp = await supertest + .get('/api/timeline/resolve') + .query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' }); + + expect(resp.body.data.timeline.notes[0].eventId).to.be('Edo00XsBEVtyvU-8LGNe'); + }); + + it('should return notes with the timelineId matching request id', async () => { + const resp = await supertest + .get('/api/timeline/resolve') + .query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' }); + + expect(resp.body.data.timeline.notes[0].timelineId).to.be( + '6484cc90-126e-11ec-83d2-db1096c73738' + ); + expect(resp.body.data.timeline.notes[1].timelineId).to.be( + '6484cc90-126e-11ec-83d2-db1096c73738' + ); + }); + }); - it('Make sure that we get Timeline template data', async () => { - const titleToSaved = 'hello timeline template'; - await createBasicTimelineTemplate(supertest, titleToSaved); + describe('pinned events', () => { + it('should pinned events with eventId', async () => { + const resp = await supertest + .get('/api/timeline/resolve') + .query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' }); - const resp = await supertest - .get('/api/timelines?timeline_type=template') - .set('kbn-xsrf', 'true'); + expect(resp.body.data.timeline.pinnedEventsSaveObject[0].eventId).to.be( + 'DNo00XsBEVtyvU-8LGNe' + ); + expect(resp.body.data.timeline.pinnedEventsSaveObject[1].eventId).to.be( + 'Edo00XsBEVtyvU-8LGNe' + ); + }); - const templates: SavedTimeline[] = resp.body.timeline; + it('should return pinned events with the timelineId matching request id', async () => { + const resp = await supertest + .get('/api/timeline/resolve') + .query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' }); - expect(templates.length).to.greaterThan(0); - expect(templates.filter((t) => t.timelineType === TimelineType.default).length).to.equal(0); + expect(resp.body.data.timeline.pinnedEventsSaveObject[0].timelineId).to.be( + '6484cc90-126e-11ec-83d2-db1096c73738' + ); + expect(resp.body.data.timeline.pinnedEventsSaveObject[1].timelineId).to.be( + '6484cc90-126e-11ec-83d2-db1096c73738' + ); + }); + }); }); }); } diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index d402a74287f9..efe159b36e3d 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -37,6 +37,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./correlations/latency')); }); + describe('metadata/event_metadata', function () { + loadTestFile(require.resolve('./metadata/event_metadata')); + }); + describe('metrics_charts/metrics_charts', function () { loadTestFile(require.resolve('./metrics_charts/metrics_charts')); }); diff --git a/x-pack/test/apm_api_integration/tests/metadata/event_metadata.ts b/x-pack/test/apm_api_integration/tests/metadata/event_metadata.ts new file mode 100644 index 000000000000..d979f0bad1ec --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/metadata/event_metadata.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { PROCESSOR_EVENT } from '../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../plugins/apm/common/processor_event'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const esClient = getService('es'); + + async function getLastDocId(processorEvent: ProcessorEvent) { + const response = await esClient.search<{ + [key: string]: { id: string }; + }>({ + index: ['apm-*'], + body: { + query: { + bool: { + filter: [{ term: { [PROCESSOR_EVENT]: processorEvent } }], + }, + }, + size: 1, + sort: { + '@timestamp': 'desc', + }, + }, + }); + + return response.body.hits.hits[0]._source![processorEvent].id; + } + + registry.when('Event metadata', { config: 'basic', archives: ['apm_8.0.0'] }, () => { + it('fetches transaction metadata', async () => { + const id = await getLastDocId(ProcessorEvent.transaction); + + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.transaction, + id, + }, + }, + }); + + expect(body).keys('metadata').ok(); + + expect( + Object.keys(body.metadata).filter((key) => { + return Array.isArray(body.metadata[key]); + }) + ); + + expect(body.metadata).keys( + '@timestamp', + 'agent.name', + 'transaction.name', + 'transaction.type', + 'service.name' + ); + }); + + it('fetches error metadata', async () => { + const id = await getLastDocId(ProcessorEvent.error); + + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.error, + id, + }, + }, + }); + + expect(body).keys('metadata').ok(); + + expect( + Object.keys(body.metadata).filter((key) => { + return Array.isArray(body.metadata[key]); + }) + ); + + expect(body.metadata).keys( + '@timestamp', + 'agent.name', + 'error.grouping_key', + 'error.grouping_name', + 'service.name' + ); + }); + + it('fetches span metadata', async () => { + const id = await getLastDocId(ProcessorEvent.span); + + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.span, + id, + }, + }, + }); + + expect(body).keys('metadata').ok(); + + expect( + Object.keys(body.metadata).filter((key) => { + return Array.isArray(body.metadata[key]); + }) + ); + + expect(body.metadata).keys( + '@timestamp', + 'agent.name', + 'span.name', + 'span.type', + 'service.name' + ); + }); + }); +} diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index 0cf4ecefe8db..5c6d68466dde 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -28,13 +28,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - // FLAKY https://github.com/elastic/kibana/issues/113067 - describe.skip('spaces', () => { + describe('spaces', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); }); - describe('space with no features disabled', () => { + // FLAKY: https://github.com/elastic/kibana/issues/60559 + describe.skip('space with no features disabled', () => { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index ec649935adec..5d8c2aff3ed5 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -16,17 +16,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataGrid = getService('dataGrid'); const panelActions = getService('dashboardPanelActions'); const panelActionsTimeRange = getService('dashboardPanelTimeRange'); + const queryBar = getService('queryBar'); + const filterBar = getService('filterBar'); const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; + const defaultSettings = { + defaultIndex: 'logstash-*', + 'doc_table:legacy': false, + }; - // FLAKY https://github.com/elastic/kibana/issues/104578 - describe.skip('Discover Saved Searches', () => { + const setTimeRange = async () => { + const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; + const toTime = 'Aug 23, 2019 @ 16:18:51.821'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + }; + + describe('Discover Saved Searches', () => { before('initialize tests', async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await kibanaServer.importExport.load(ecommerceSOPath); - await kibanaServer.uiSettings.update({ 'doc_table:legacy': false }); + await kibanaServer.uiSettings.update(defaultSettings); }); + after('clean up archives', async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); await kibanaServer.importExport.unload(ecommerceSOPath); await kibanaServer.uiSettings.unset('doc_table:legacy'); }); @@ -35,9 +49,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should be possible to customize time range for saved searches on dashboards', async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); - const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; - const toTime = 'Aug 23, 2019 @ 16:18:51.821'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await setTimeRange(); await dashboardAddPanel.clickOpenAddPanel(); await dashboardAddPanel.addSavedSearch('Ecommerce Data'); expect(await dataGrid.getDocCount()).to.be(500); @@ -50,5 +62,35 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await dataGrid.hasNoResults()).to.be(true); }); }); + + it(`should unselect saved search when navigating to a 'new'`, async function () { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('ecommerce'); + await setTimeRange(); + await filterBar.addFilter('category', 'is', `Men's Shoes`); + await queryBar.setQuery('customer_gender:MALE'); + + await PageObjects.discover.saveSearch('test-unselect-saved-search'); + + await queryBar.submitQuery(); + + expect(await filterBar.hasFilter('category', `Men's Shoes`)).to.be(true); + expect(await queryBar.getQueryString()).to.eql('customer_gender:MALE'); + + await PageObjects.discover.clickNewSearchButton(); + + expect(await filterBar.hasFilter('category', `Men's Shoes`)).to.be(false); + expect(await queryBar.getQueryString()).to.eql(''); + + await PageObjects.discover.selectIndexPattern('logstash-*'); + + expect(await filterBar.hasFilter('category', `Men's Shoes`)).to.be(false); + expect(await queryBar.getQueryString()).to.eql(''); + + await PageObjects.discover.selectIndexPattern('ecommerce'); + + expect(await filterBar.hasFilter('category', `Men's Shoes`)).to.be(false); + expect(await queryBar.getQueryString()).to.eql(''); + }); }); } diff --git a/x-pack/test/functional/apps/lens/heatmap.ts b/x-pack/test/functional/apps/lens/heatmap.ts index ddc4130d388c..deca06b6b351 100644 --- a/x-pack/test/functional/apps/lens/heatmap.ts +++ b/x-pack/test/functional/apps/lens/heatmap.ts @@ -13,8 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const elasticChart = getService('elasticChart'); const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/113043 - describe.skip('lens heatmap', () => { + describe('lens heatmap', () => { before(async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); @@ -74,8 +73,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.openPalettePanel('lnsHeatmap'); await testSubjects.setValue('lnsPalettePanel_dynamicColoring_stop_value_0', '10', { clearWithKeyboard: true, + typeCharByChar: true, }); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); const debugState = await PageObjects.lens.getCurrentChartDebugState(); diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/persistent_context.ts index 90a7b9fe83c1..e7b99ad804cd 100644 --- a/x-pack/test/functional/apps/lens/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/persistent_context.ts @@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange( - 'Sep 06, 2015 @ 06:31:44.000', + 'Sep 6, 2015 @ 06:31:44.000', 'Sep 18, 2025 @ 06:31:44.000' ); await filterBar.addFilter('ip', 'is', '97.220.3.248'); @@ -45,7 +45,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should remember time range and pinned filters from discover', async () => { await PageObjects.lens.goToTimeRange( - 'Sep 07, 2015 @ 06:31:44.000', + 'Sep 7, 2015 @ 06:31:44.000', 'Sep 19, 2025 @ 06:31:44.000' ); await filterBar.toggleFilterEnabled('ip'); diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json index 1685d909eee8..880a81a8fc20 100644 --- a/x-pack/test/functional/es_archives/alerts/data.json +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -451,4 +451,44 @@ "updated_at": "2020-06-17T15:35:39.839Z" } } -} \ No newline at end of file +} + +{ + "type": "doc", + "value": { + "id": "alert:d7a8c6a1-9394-48df-a634-d5457c35d747", + "index": ".kibana_1", + "source": { + "alert" : { + "name" : "test upgrade of ruleAlertId", + "alertTypeId" : "siem.notifications", + "consumer" : "alertsFixture", + "params" : { + "ruleAlertId" : "1a4ed6ae-3c89-44b2-999d-db554144504c" + }, + "schedule" : { + "interval" : "1m" + }, + "enabled" : true, + "actions" : [ ], + "throttle" : null, + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "elastic", + "updatedBy" : "elastic", + "createdAt" : "2021-07-27T20:42:55.896Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "scheduledTaskId" : null, + "tags": [] + }, + "type" : "alert", + "migrationVersion" : { + "alert" : "7.8.0" + }, + "updated_at" : "2021-08-13T23:00:11.985Z", + "references": [ + ] + } + } +} diff --git a/x-pack/test/observability_functional/apps/observability/exploratory_view.ts b/x-pack/test/observability_functional/apps/observability/exploratory_view.ts new file mode 100644 index 000000000000..8f27f20ce30e --- /dev/null +++ b/x-pack/test/observability_functional/apps/observability/exploratory_view.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Path from 'path'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['observability', 'common', 'header']); + const esArchiver = getService('esArchiver'); + const find = getService('find'); + + const testSubjects = getService('testSubjects'); + + const rangeFrom = '2021-01-17T16%3A46%3A15.338Z'; + const rangeTo = '2021-01-19T17%3A01%3A32.309Z'; + + // Failing: See https://github.com/elastic/kibana/issues/106934 + describe.skip('ExploratoryView', () => { + before(async () => { + await esArchiver.loadIfNeeded( + Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0') + ); + + await esArchiver.loadIfNeeded( + Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0') + ); + + await esArchiver.loadIfNeeded( + Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_test_data') + ); + + await PageObjects.common.navigateToApp('ux', { + search: `?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await esArchiver.unload( + Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0') + ); + + await esArchiver.unload( + Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0') + ); + }); + + it('should able to open exploratory view from ux app', async () => { + await testSubjects.exists('uxAnalyzeBtn'); + await testSubjects.click('uxAnalyzeBtn'); + expect(await find.existsByCssSelector('.euiBasicTable')).to.eql(true); + }); + + it('renders lens visualization', async () => { + expect(await testSubjects.exists('lnsVisualizationContainer')).to.eql(true); + + expect( + await find.existsByCssSelector('div[data-title="Prefilled from exploratory view app"]') + ).to.eql(true); + + expect((await find.byCssSelector('dd')).getVisibleText()).to.eql(true); + }); + + it('can do a breakdown per series', async () => { + await testSubjects.click('seriesBreakdown'); + + expect(await find.existsByCssSelector('[id="user_agent.name"]')).to.eql(true); + + await find.clickByCssSelector('[id="user_agent.name"]'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + + expect(await find.existsByCssSelector('[title="Chrome Mobile iOS"]')).to.eql(true); + expect(await find.existsByCssSelector('[title="Mobile Safari"]')).to.eql(true); + }); + }); +} diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts index 019fb0994715..b163d4d6bb8d 100644 --- a/x-pack/test/observability_functional/apps/observability/index.ts +++ b/x-pack/test/observability_functional/apps/observability/index.ts @@ -8,9 +8,10 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('Observability specs', function () { + describe('ObservabilityApp', function () { this.tags('ciGroup6'); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./exploratory_view')); loadTestFile(require.resolve('./alerts')); loadTestFile(require.resolve('./alerts/workflow_status')); loadTestFile(require.resolve('./alerts/pagination')); diff --git a/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts b/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts index 8d078994eb0e..3db2e2ebdce0 100644 --- a/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts +++ b/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts @@ -5,10 +5,8 @@ * 2.0. */ -import base64url from 'base64url'; -import { createHash } from 'crypto'; +import { createHash, createSign } from 'crypto'; import fs from 'fs'; -import jwt from 'jsonwebtoken'; import url from 'url'; export function getStateAndNonce(urlWithStateAndNonce: string) { @@ -16,16 +14,20 @@ export function getStateAndNonce(urlWithStateAndNonce: string) { return { state: parsedQuery.state as string, nonce: parsedQuery.nonce as string }; } +function fromBase64(base64: string) { + return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); +} + export function createTokens(userId: string, nonce: string) { - const signingKey = fs.readFileSync(require.resolve('./jwks_private.pem')); - const iat = Math.floor(Date.now() / 1000); + const idTokenHeader = fromBase64( + Buffer.from(JSON.stringify({ alg: 'RS256' })).toString('base64') + ); + const iat = Math.floor(Date.now() / 1000); const accessToken = `valid-access-token${userId}`; const accessTokenHashBuffer = createHash('sha256').update(accessToken).digest(); - - return { - accessToken, - idToken: jwt.sign( + const idTokenBody = fromBase64( + Buffer.from( JSON.stringify({ iss: 'https://test-op.elastic.co', sub: `user${userId}`, @@ -34,10 +36,19 @@ export function createTokens(userId: string, nonce: string) { exp: iat + 3600, iat, // See more details on `at_hash` at https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken - at_hash: base64url(accessTokenHashBuffer.slice(0, accessTokenHashBuffer.length / 2)), - }), - signingKey, - { algorithm: 'RS256' } - ), - }; + at_hash: fromBase64( + accessTokenHashBuffer.slice(0, accessTokenHashBuffer.length / 2).toString('base64') + ), + }) + ).toString('base64') + ); + + const idToken = `${idTokenHeader}.${idTokenBody}`; + + const signingKey = fs.readFileSync(require.resolve('./jwks_private.pem')); + const idTokenSignature = fromBase64( + createSign('RSA-SHA256').update(idToken).sign(signingKey, 'base64') + ); + + return { accessToken, idToken: `${idToken}.${idTokenSignature}` }; } diff --git a/x-pack/test/security_solution_cypress/es_archives/linux_process/data.json b/x-pack/test/security_solution_cypress/es_archives/linux_process/data.json new file mode 100644 index 000000000000..ed29f3fe3e4e --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/linux_process/data.json @@ -0,0 +1,135 @@ +{ + "type": "doc", + "value": { + "id": "qxnqn3sBBf0WZxoXk7tg", + "index": "run-parts", + "source": { + "@timestamp": "2021-09-01T05:52:29.9451497Z", + "agent": { + "id": "cda623db-f791-4869-a63d-5b8352dfaa56", + "type": "endpoint", + "version": "7.14.0" + }, + "data_stream": { + "dataset": "endpoint.events.process", + "namespace": "default", + "type": "logs" + }, + "ecs": { + "version": "1.6.0" + }, + "elastic": { + "agent": { + "id": "cda623db-f791-4869-a63d-5b8352dfaa56" + } + }, + "event": { + "action": "exec", + "agent_id_status": "verified", + "category": [ + "process" + ], + "created": "2021-09-01T05:52:29.9451497Z", + "dataset": "endpoint.events.process", + "id": "MGwI0NpfzFKkX6gW+++++CVd", + "ingested": "2021-09-01T05:52:35.677424686Z", + "kind": "event", + "module": "endpoint", + "sequence": 3523, + "type": [ + "start" + ] + }, + "group": { + "Ext": { + "real": { + "id": 0, + "name": "root" + } + }, + "id": 0, + "name": "root" + }, + "host": { + "architecture": "x86_64", + "hostname": "localhost", + "id": "f5c59e5f0c963f828782bc413653d324", + "ip": [ + "127.0.0.1", + "::1" + ], + "mac": [ + "00:16:3e:10:96:79" + ], + "name": "localhost", + "os": { + "Ext": { + "variant": "Debian" + }, + "family": "debian", + "full": "Debian 10", + "kernel": "4.19.0-17-amd64 #1 SMP Debian 4.19.194-3 (2021-07-18)", + "name": "Linux", + "platform": "debian", + "version": "10" + } + }, + "message": "Endpoint process event", + "process": { + "Ext": { + "ancestry": [ + "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTM2Njk1MDAw", + "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTMwNzYyMTAw", + "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDktMTMyNzQ5NDkxNDkuOTI4OTI0ODAw", + "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDktMTMyNzQ5NDkxNDkuOTI3NDgwMzAw", + "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDEtMTMyNzQ5NDkxNDYuNTI3ODA5NTAw", + "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDEtMTMyNzQ5NDkxNDYuNTIzNzEzOTAw", + "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTczOC0xMzI3NDk0ODg3OS4yNzgyMjQwMDA=", + "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTczOC0xMzI3NDk0ODg3OS4yNTQ1MTUzMDA=", + "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEtMTMyNzQ5NDg4NjkuMA==" + ] + }, + "args": [ + "run-parts", + "--lsbsysinit", + "/etc/update-motd.d" + ], + "args_count": 3, + "command_line": "run-parts --lsbsysinit /etc/update-motd.d", + "entity_id": "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTQ1MTQ5NzAw", + "executable": "/usr/bin/run-parts", + "hash": { + "md5": "c83b0578484bf5267893d795b55928bd", + "sha1": "46b6e74e28e5daf69c1dd0f18a8e911ae2922dda", + "sha256": "3346b4d47c637a8c02cb6865eee42d2a5aa9c4e46c6371a9143621348d27420f" + }, + "name": "run-parts", + "parent": { + "args": [ + "sh", + "-c", + "/usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d > /run/motd.dynamic.new" + ], + "args_count": 0, + "command_line": "sh -c /usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d > /run/motd.dynamic.new", + "entity_id": "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTM2Njk1MDAw", + "executable": "/", + "name": "", + "pid": 1349 + }, + "pid": 1350 + }, + "user": { + "Ext": { + "real": { + "id": 0, + "name": "root" + } + }, + "id": 0, + "name": "root" + } + }, + "type": "_doc" + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/linux_process/mappings.json b/x-pack/test/security_solution_cypress/es_archives/linux_process/mappings.json new file mode 100644 index 000000000000..d244defbdab0 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/linux_process/mappings.json @@ -0,0 +1,935 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "run-parts", + "mappings": { + "_data_stream_timestamp": { + "enabled": true + }, + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "constant_keyword", + "value": "endpoint.events.process" + }, + "namespace": { + "type": "constant_keyword", + "value": "default" + }, + "type": { + "type": "constant_keyword", + "value": "logs" + } + } + }, + "destination": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "agent_id_status": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "normalizer": "lowercase", + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "normalizer": "lowercase", + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "message": { + "type": "text" + }, + "package": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + }, + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "authentication_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + }, + "type": "nested" + }, + "defense_evasions": { + "ignore_above": 1024, + "type": "keyword" + }, + "dll": { + "properties": { + "Ext": { + "properties": { + "mapped_address": { + "type": "unsigned_long" + }, + "mapped_size": { + "type": "unsigned_long" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "session": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "elevation": { + "type": "boolean" + }, + "elevation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "caseless": { + "ignore_above": 1024, + "normalizer": "lowercase", + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "normalizer": "lowercase", + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "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" + } + } + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "normalizer": "lowercase", + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "Ext": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + }, + "type": "nested" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "real": { + "properties": { + "pid": { + "type": "long" + } + } + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "caseless": { + "ignore_above": 1024, + "normalizer": "lowercase", + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "normalizer": "lowercase", + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "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" + } + } + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "normalizer": "lowercase", + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "caseless": { + "ignore_above": 1024, + "normalizer": "lowercase", + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "caseless": { + "ignore_above": 1024, + "normalizer": "lowercase", + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/runner.ts b/x-pack/test/security_solution_cypress/runner.ts index f93e20ec382c..4283b85af0c1 100644 --- a/x-pack/test/security_solution_cypress/runner.ts +++ b/x-pack/test/security_solution_cypress/runner.ts @@ -26,22 +26,10 @@ export async function SecuritySolutionCypressCliTestRunner({ getService }: FtrPr cwd: resolve(__dirname, '../../plugins/security_solution'), env: { FORCE_COLOR: '1', - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_protocol: config.get('servers.kibana.protocol'), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_hostname: config.get('servers.kibana.hostname'), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_configport: config.get('servers.kibana.port'), + CYPRESS_BASE_URL: Url.format(config.get('servers.kibana')), CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), - CYPRESS_KIBANA_URL: Url.format({ - protocol: config.get('servers.kibana.protocol'), - hostname: config.get('servers.kibana.hostname'), - port: config.get('servers.kibana.port'), - }), ...process.env, }, wait: true, @@ -65,22 +53,10 @@ export async function SecuritySolutionCypressCliFirefoxTestRunner({ cwd: resolve(__dirname, '../../plugins/security_solution'), env: { FORCE_COLOR: '1', - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_protocol: config.get('servers.kibana.protocol'), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_hostname: config.get('servers.kibana.hostname'), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_configport: config.get('servers.kibana.port'), + CYPRESS_BASE_URL: Url.format(config.get('servers.kibana')), CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), - CYPRESS_KIBANA_URL: Url.format({ - protocol: config.get('servers.kibana.protocol'), - hostname: config.get('servers.kibana.hostname'), - port: config.get('servers.kibana.port'), - }), ...process.env, }, wait: true, @@ -126,22 +102,10 @@ export async function SecuritySolutionCypressVisualTestRunner({ getService }: Ft cwd: resolve(__dirname, '../../plugins/security_solution'), env: { FORCE_COLOR: '1', - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_protocol: config.get('servers.kibana.protocol'), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_hostname: config.get('servers.kibana.hostname'), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_configport: config.get('servers.kibana.port'), + CYPRESS_BASE_URL: Url.format(config.get('servers.kibana')), CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), - CYPRESS_KIBANA_URL: Url.format({ - protocol: config.get('servers.kibana.protocol'), - hostname: config.get('servers.kibana.hostname'), - port: config.get('servers.kibana.port'), - }), ...process.env, }, wait: true, @@ -153,6 +117,7 @@ export async function SecuritySolutionCypressUpgradeCliTestRunner({ getService, }: FtrProviderContext) { const log = getService('log'); + const config = getService('config'); await withProcRunner(log, async (procs) => { await procs.run('cypress', { @@ -161,18 +126,10 @@ export async function SecuritySolutionCypressUpgradeCliTestRunner({ cwd: resolve(__dirname, '../../plugins/security_solution'), env: { FORCE_COLOR: '1', - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_baseUrl: process.env.TEST_KIBANA_URL, - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_protocol: process.env.TEST_KIBANA_PROTOCOL, - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_hostname: process.env.TEST_KIBANA_HOSTNAME, - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_configport: process.env.TEST_KIBANA_PORT, - CYPRESS_ELASTICSEARCH_URL: process.env.TEST_ES_URL, - CYPRESS_ELASTICSEARCH_USERNAME: process.env.TEST_ES_USER, - CYPRESS_ELASTICSEARCH_PASSWORD: process.env.TEST_ES_PASS, - CYPRESS_KIBANA_URL: process.env.TEST_KIBANA_URL, + CYPRESS_BASE_URL: Url.format(config.get('servers.kibana')), + CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), + CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), + CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), ...process.env, }, wait: true, diff --git a/yarn.lock b/yarn.lock index 14cf34cae847..9f6ccfffb811 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2404,15 +2404,6 @@ async-retry "^1.2.3" strip-ansi "^5.2.0" -"@elastic/good@^9.0.1-kibana3": - version "9.0.1-kibana3" - resolved "https://registry.yarnpkg.com/@elastic/good/-/good-9.0.1-kibana3.tgz#a70c2b30cbb4f44d1cf4a464562e0680322eac9b" - integrity sha512-UtPKr0TmlkL1abJfO7eEVUTqXWzLKjMkz+65FvxU/Ub9kMAr4No8wHLRfDHFzBkWoDIbDWygwld011WzUnea1Q== - dependencies: - "@hapi/hoek" "9.x.x" - "@hapi/oppsy" "3.x.x" - "@hapi/validate" "1.x.x" - "@elastic/makelogs@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@elastic/makelogs/-/makelogs-6.0.0.tgz#d6d74d5d0f020123c54160370d49ca5e0aab1fe1" @@ -2897,14 +2888,6 @@ resolved "https://registry.yarnpkg.com/@hapi/file/-/file-2.0.0.tgz#2ecda37d1ae9d3078a67c13b7da86e8c3237dfb9" integrity sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ== -"@hapi/good-squeeze@6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@hapi/good-squeeze/-/good-squeeze-6.0.0.tgz#bb72d6869cd7398b615a6b7270f630dc4f76aebf" - integrity sha512-UgHAF9Lm8fJPzgf2HymtowOwNc1+IL+p08YTVR+XA4d8nmyE1t9x3RLA4riqldnOKHkVqGakJ1jGqUG7jk77Cg== - dependencies: - "@hapi/hoek" "9.x.x" - fast-safe-stringify "2.x.x" - "@hapi/h2o2@^9.1.0": version "9.1.0" resolved "https://registry.yarnpkg.com/@hapi/h2o2/-/h2o2-9.1.0.tgz#b223f4978b6f2b0d7d9db10a84a567606c4c3551" @@ -2992,13 +2975,6 @@ "@hapi/hoek" "^9.0.4" "@hapi/vise" "^4.0.0" -"@hapi/oppsy@3.x.x": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@hapi/oppsy/-/oppsy-3.0.0.tgz#1ae397e200e86d0aa41055f103238ed8652947ca" - integrity sha512-0kfUEAqIi21GzFVK2snMO07znMEBiXb+/pOx1dmgOO9TuvFstcfmHU5i56aDfiFP2DM5WzQCU2UWc2gK1lMDhQ== - dependencies: - "@hapi/hoek" "9.x.x" - "@hapi/pez@^5.0.1": version "5.0.3" resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-5.0.3.tgz#b75446e6fef8cbb16816573ab7da1b0522e7a2a1" @@ -3750,10 +3726,6 @@ version "0.0.0" uid "" -"@kbn/legacy-logging@link:bazel-bin/packages/kbn-legacy-logging": - version "0.0.0" - uid "" - "@kbn/logging@link:bazel-bin/packages/kbn-logging": version "0.0.0" uid "" @@ -4722,11 +4694,6 @@ 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" - integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== - "@sindresorhus/is@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4" @@ -6394,13 +6361,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818" integrity sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA== -"@types/jsonwebtoken@^8.5.5": - version "8.5.5" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.5.tgz#da5f2f4baee88f052ef3e4db4c1a0afb46cff22c" - integrity sha512-OGqtHQ7N5/Ap/TUwO6IgHDuLiAoTmHhGpNvgkCm/F4N6pKzx/RBSfr2OXZSwC6vkfnsEdb6+7DNZVtiXiwdwFw== - dependencies: - "@types/node" "*" - "@types/keyv@*": version "3.1.1" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" @@ -8104,13 +8064,6 @@ arch@^2.2.0: resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== -archive-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/archive-type/-/archive-type-4.0.0.tgz#f92e72233056dfc6969472749c267bdb046b1d70" - integrity sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA= - dependencies: - file-type "^4.2.0" - archiver-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" @@ -9235,7 +9188,7 @@ base64-js@^1.0.2, base64-js@^1.1.2, base64-js@^1.2.0, base64-js@^1.3.0, base64-j resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== -base64url@^3.0.0, base64url@^3.0.1: +base64url@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== @@ -9309,17 +9262,6 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -bin-build@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bin-build/-/bin-build-3.0.0.tgz#c5780a25a8a9f966d8244217e6c1f5082a143861" - integrity sha512-jcUOof71/TNAI2uM5uoUaDq2ePcVBQ3R/qhxAz1rX7UfvduAL/RXD3jXzvn8cVcDJdGVkiR1shal3OH0ImpuhA== - dependencies: - decompress "^4.0.0" - download "^6.2.2" - execa "^0.7.0" - p-map-series "^1.0.0" - tempfile "^2.0.0" - binary-extensions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" @@ -9330,14 +9272,6 @@ binary-search@^1.3.3: resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== -bl@^1.0.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" - integrity sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA== - dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" - bl@^4.0.1, bl@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" @@ -9800,19 +9734,6 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= -buffer-alloc-unsafe@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" - integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== - -buffer-alloc@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" - integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== - dependencies: - buffer-alloc-unsafe "^1.1.0" - buffer-fill "^1.0.0" - buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -9833,11 +9754,6 @@ buffer-equal@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= -buffer-fill@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" - integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= - buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -9975,19 +9891,6 @@ cacheable-lookup@^5.0.3: resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3" integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w== -cacheable-request@^2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" - integrity sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0= - dependencies: - clone-response "1.0.2" - get-stream "3.0.0" - http-cache-semantics "3.8.1" - keyv "3.0.0" - lowercase-keys "1.0.0" - 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" @@ -10186,16 +10089,6 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -caw@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/caw/-/caw-2.0.1.tgz#6c3ca071fc194720883c2dc5da9b074bfc7e9e95" - integrity sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA== - dependencies: - get-proxy "^2.0.0" - isurl "^1.0.0-alpha5" - tunnel-agent "^0.6.0" - url-to-options "^1.0.1" - ccount@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" @@ -10649,7 +10542,7 @@ clone-regexp@^2.1.0: dependencies: is-regexp "^2.0.0" -clone-response@1.0.2, clone-response@^1.0.2: +clone-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= @@ -10934,13 +10827,6 @@ commander@^7.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -commander@~2.8.1: - version "2.8.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" - integrity sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ= - dependencies: - graceful-readlink ">= 1.0.0" - common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -11051,7 +10937,7 @@ concaveman@*: robust-predicates "^2.0.4" tinyqueue "^2.0.3" -config-chain@^1.1.11, config-chain@^1.1.12: +config-chain@^1.1.12: version "1.1.12" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== @@ -11132,7 +11018,7 @@ container-info@^1.0.1: resolved "https://registry.yarnpkg.com/container-info/-/container-info-1.0.1.tgz#6b383cb5e197c8d921e88983388facb04124b56b" integrity sha512-wk/+uJvPHOFG+JSwQS+fw6H6yw3Oyc8Kw9L4O2MN817uA90OqJ59nlZbbLPqDudsjJ7Tetee3pwExdKpd2ahjQ== -content-disposition@0.5.3, content-disposition@^0.5.2: +content-disposition@0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== @@ -11463,15 +11349,6 @@ cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -12430,7 +12307,7 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= -decompress-response@^3.2.0, decompress-response@^3.3.0: +decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= @@ -12451,59 +12328,6 @@ decompress-response@^6.0.0: dependencies: mimic-response "^3.1.0" -decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" - integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== - dependencies: - file-type "^5.2.0" - is-stream "^1.1.0" - tar-stream "^1.5.2" - -decompress-tarbz2@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" - integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== - dependencies: - decompress-tar "^4.1.0" - file-type "^6.1.0" - is-stream "^1.1.0" - seek-bzip "^1.0.5" - unbzip2-stream "^1.0.9" - -decompress-targz@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" - integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== - dependencies: - decompress-tar "^4.1.1" - file-type "^5.2.0" - is-stream "^1.1.0" - -decompress-unzip@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" - integrity sha1-3qrM39FK6vhVePczroIQ+bSEj2k= - dependencies: - file-type "^3.8.0" - get-stream "^2.2.0" - pify "^2.3.0" - yauzl "^2.4.2" - -decompress@^4.0.0, decompress@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" - integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== - dependencies: - decompress-tar "^4.0.0" - decompress-tarbz2 "^4.0.0" - decompress-targz "^4.0.0" - decompress-unzip "^4.0.1" - graceful-fs "^4.1.10" - make-dir "^1.0.0" - pify "^2.3.0" - strip-dirs "^2.0.0" - dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -13180,40 +13004,6 @@ dotignore@^0.1.2: dependencies: minimatch "^3.0.4" -download@^6.2.2: - version "6.2.5" - resolved "https://registry.yarnpkg.com/download/-/download-6.2.5.tgz#acd6a542e4cd0bb42ca70cfc98c9e43b07039714" - integrity sha512-DpO9K1sXAST8Cpzb7kmEhogJxymyVUd5qz/vCOSyvwtp2Klj2XcDt5YUuasgxka44SxF0q5RriKIwJmQHG2AuA== - dependencies: - caw "^2.0.0" - content-disposition "^0.5.2" - decompress "^4.0.0" - ext-name "^5.0.0" - file-type "5.2.0" - filenamify "^2.0.0" - get-stream "^3.0.0" - got "^7.0.0" - make-dir "^1.0.0" - p-event "^1.0.0" - pify "^3.0.0" - -download@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/download/-/download-8.0.0.tgz#afc0b309730811731aae9f5371c9f46be73e51b1" - integrity sha512-ASRY5QhDk7FK+XrQtQyvhpDKanLluEEQtWl/J7Lxuf/b+i8RYh997QeXvL85xitrmRKVlx9c7eTrcRdq2GS4eA== - dependencies: - archive-type "^4.0.0" - content-disposition "^0.5.2" - decompress "^4.2.1" - ext-name "^5.0.0" - file-type "^11.1.0" - filenamify "^3.0.0" - get-stream "^4.1.0" - got "^8.3.1" - make-dir "^2.1.0" - p-event "^2.1.0" - pify "^4.0.1" - downshift@^3.2.10: version "3.4.8" resolved "https://registry.yarnpkg.com/downshift/-/downshift-3.4.8.tgz#06b7ad9e9c423a58e8a9049b2a00a5d19c7ef954" @@ -14354,19 +14144,6 @@ execa@4.1.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" - integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -14536,21 +14313,6 @@ express@^4.16.3, express@^4.17.0, express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" -ext-list@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" - integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA== - dependencies: - mime-db "^1.28.0" - -ext-name@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6" - integrity sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ== - dependencies: - ext-list "^2.0.0" - sort-keys-length "^1.0.0" - extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -14703,7 +14465,7 @@ fast-redact@^3.0.0: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.0.tgz#ac2f9e36c9f4976f5db9fb18c6ffbaf308cf316d" integrity sha512-a/S/Hp6aoIjx7EmugtzLqXmcNsyFszqbt6qQ99BdG61QjBZF6shNis0BYR6TsZOQ1twYc0FN2Xdhwwbv6+KD0w== -fast-safe-stringify@2.x.x, fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7: +fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7: version "2.0.8" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f" integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag== @@ -14896,36 +14658,11 @@ file-system-cache@^1.0.5: fs-extra "^0.30.0" ramda "^0.21.0" -file-type@5.2.0, file-type@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" - integrity sha1-LdvqfHP/42No365J3DOMBYwritY= - file-type@^10.9.0: version "10.9.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-10.9.0.tgz#f6c12c7cb9e6b8aeefd6917555fd4f9eadf31891" integrity sha512-9C5qtGR/fNibHC5gzuMmmgnjH3QDDLKMa8lYe9CiZVmAnI4aUaoMh40QyUPzzs0RYo837SOBKh7TYwle4G8E4w== -file-type@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-11.1.0.tgz#93780f3fed98b599755d846b99a1617a2ad063b8" - integrity sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g== - -file-type@^3.8.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" - integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek= - -file-type@^4.2.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-4.4.0.tgz#1b600e5fca1fbdc6e80c0a70c71c8dba5f7906c5" - integrity sha1-G2AOX8ofvcboDApwxxyNul95BsU= - -file-type@^6.1.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" - integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== - file-type@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-9.0.0.tgz#a68d5ad07f486414dfb2c8866f73161946714a18" @@ -14938,29 +14675,6 @@ filelist@^1.0.1: dependencies: minimatch "^3.0.4" -filename-reserved-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" - integrity sha1-q/c9+rc10EVECr/qLZHzieu/oik= - -filenamify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-2.0.0.tgz#bd162262c0b6e94bfbcdcf19a3bbb3764f785695" - integrity sha1-vRYiYsC26Uv7zc8Zo7uzdk94VpU= - dependencies: - filename-reserved-regex "^2.0.0" - strip-outer "^1.0.0" - trim-repeated "^1.0.0" - -filenamify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-3.0.0.tgz#9603eb688179f8c5d40d828626dcbb92c3a4672c" - integrity sha512-5EFZ//MsvJgXjBAFJ+Bh2YaCTRF/VP1YOmGrgt+KJ4SFRLjI87EIdwLLuT6wQX0I4F9W41xutobzczjsOKlI/g== - dependencies: - filename-reserved-regex "^2.0.0" - strip-outer "^1.0.0" - trim-repeated "^1.0.0" - filesize@6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00" @@ -15330,7 +15044,7 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -from2@^2.1.0, from2@^2.1.1: +from2@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= @@ -15598,13 +15312,6 @@ get-port@^5.0.0: resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== -get-proxy@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/get-proxy/-/get-proxy-2.1.0.tgz#349f2b4d91d44c4d4d4e9cba2ad90143fac5ef93" - integrity sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw== - dependencies: - npm-conf "^1.1.0" - get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -15620,19 +15327,6 @@ get-stdin@^8.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== -get-stream@3.0.0, get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - -get-stream@^2.2.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" - integrity sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4= - dependencies: - object-assign "^4.0.1" - pinkie-promise "^2.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" @@ -16036,49 +15730,6 @@ got@^3.2.0: read-all-stream "^3.0.0" timed-out "^2.0.0" -got@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a" - integrity sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw== - dependencies: - decompress-response "^3.2.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - is-plain-obj "^1.1.0" - is-retry-allowed "^1.0.0" - is-stream "^1.0.0" - isurl "^1.0.0-alpha5" - lowercase-keys "^1.0.0" - p-cancelable "^0.3.0" - p-timeout "^1.1.1" - safe-buffer "^5.0.1" - timed-out "^4.0.0" - url-parse-lax "^1.0.0" - url-to-options "^1.0.1" - -got@^8.3.1: - version "8.3.2" - resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" - integrity sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw== - dependencies: - "@sindresorhus/is" "^0.7.0" - cacheable-request "^2.1.1" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - into-stream "^3.1.0" - is-retry-allowed "^1.1.0" - isurl "^1.0.0-alpha5" - lowercase-keys "^1.0.0" - mimic-response "^1.0.0" - p-cancelable "^0.4.0" - p-timeout "^2.0.1" - pify "^3.0.0" - safe-buffer "^5.1.1" - timed-out "^4.0.1" - 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" @@ -16101,16 +15752,11 @@ graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, g resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== -graceful-fs@^4.1.10, graceful-fs@^4.2.3, graceful-fs@^4.2.6: +graceful-fs@^4.2.3, graceful-fs@^4.2.6: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= - graphlib@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da" @@ -16349,23 +15995,11 @@ has-glob@^1.0.0: dependencies: is-glob "^3.0.0" -has-symbol-support-x@^1.4.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" - integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== - has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== -has-to-string-tag-x@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" - integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw== - dependencies: - has-symbol-support-x "^1.4.1" - has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -16824,11 +16458,6 @@ htmlparser2@~3.3.0: domutils "1.1" readable-stream "1.0" -http-cache-semantics@3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" - integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== - http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -17312,14 +16941,6 @@ intl@^1.2.5: resolved "https://registry.yarnpkg.com/intl/-/intl-1.2.5.tgz#82244a2190c4e419f8371f5aa34daa3420e2abde" integrity sha1-giRKIZDE5Bn4Nx9ao02qNCDiq94= -into-stream@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" - integrity sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY= - dependencies: - from2 "^2.1.1" - p-is-promise "^1.1.0" - invariant@2.2.4, invariant@^2.1.0, invariant@^2.1.1, invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -17676,13 +17297,6 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== -is-invalid-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-invalid-path/-/is-invalid-path-0.1.0.tgz#307a855b3cf1a938b44ea70d2c61106053714f34" - integrity sha1-MHqFWzzxqTi0TqcNLGEQYFNxTzQ= - dependencies: - is-glob "^2.0.0" - is-lambda@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" @@ -17701,11 +17315,6 @@ is-native@^1.0.1: is-nil "^1.0.0" to-source-code "^1.0.0" -is-natural-number@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" - integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg= - is-negated-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" @@ -17880,11 +17489,6 @@ is-resolvable@^1.0.0: resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== -is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" - integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== - is-root@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" @@ -17961,13 +17565,6 @@ is-valid-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= -is-valid-path@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-valid-path/-/is-valid-path-0.1.1.tgz#110f9ff74c37f663e1ec7915eb451f2db93ac9df" - integrity sha1-EQ+f90w39mPh7HkV60UfLbk6yd8= - dependencies: - is-invalid-path "^0.1.0" - is-weakmap@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" @@ -18173,14 +17770,6 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -isurl@^1.0.0-alpha5: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" - integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w== - dependencies: - has-to-string-tag-x "^1.2.0" - is-object "^1.0.1" - iterate-iterator@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6" @@ -19171,7 +18760,7 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= -jsonwebtoken@^8.3.0, jsonwebtoken@^8.5.1: +jsonwebtoken@^8.3.0: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== @@ -19277,7 +18866,7 @@ kea@^2.4.2: resolved "https://registry.yarnpkg.com/kea/-/kea-2.4.2.tgz#53af42702f2c8962422e456e5dd943391bad26e9" integrity sha512-cdGds/gsJsbo/KbVAMk5/tTr229eDibVT1wmPPxPO/10zYb8GFoP3udBIQb+Hop5qGEu2wIHVdXwJvXqSS8JAg== -keyv@3.0.0, keyv@^3.0.0: +keyv@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" integrity sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA== @@ -20048,11 +19637,6 @@ lower-case@^2.0.1: dependencies: tslib "^1.10.0" -lowercase-keys@1.0.0: - version "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.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -20071,7 +19655,7 @@ lowlight@^1.14.0: fault "^1.0.0" highlight.js "~10.4.0" -lru-cache@^4.0.0, lru-cache@^4.0.1, lru-cache@^4.1.5: +lru-cache@^4.0.0, lru-cache@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -20117,13 +19701,6 @@ magic-string@0.25.1: dependencies: sourcemap-codec "^1.4.1" -make-dir@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" - integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== - dependencies: - pify "^3.0.0" - make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -20644,11 +20221,6 @@ mime-db@1.44.0, mime-db@1.x.x, "mime-db@>= 1.40.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-db@^1.28.0: - version "1.49.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" - integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== - mime-types@^2.0.1, mime-types@^2.1.12, mime-types@^2.1.26, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" @@ -21490,19 +21062,6 @@ node-jose@1.1.0: node-forge "^0.7.6" uuid "^3.3.2" -node-jq@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/node-jq/-/node-jq-2.0.0.tgz#f0777ee2be6f6bfcf0ef58cc44689c422def6d97" - integrity sha512-gp8Xkr5RlZrXVK+VuUaF0JKtsGrElHb1hOjugdJFiCMHUup1OqsSBXZyeuwPkpty3P9taFHJmw4uzjQFIMFv4g== - dependencies: - bin-build "^3.0.0" - download "^8.0.0" - is-valid-path "^0.1.1" - joi "^17.4.0" - strip-eof "^2.0.0" - strip-final-newline "^2.0.0" - tempfile "^3.0.0" - node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" @@ -21684,15 +21243,6 @@ normalize-selector@^0.2.0: resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03" integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM= -normalize-url@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" - integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== - dependencies: - prepend-http "^2.0.0" - query-string "^5.0.1" - sort-keys "^2.0.0" - normalize-url@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" @@ -21710,14 +21260,6 @@ now-and-later@^2.0.0: dependencies: once "^1.3.2" -npm-conf@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9" - integrity sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw== - dependencies: - config-chain "^1.1.11" - pify "^3.0.0" - npm-normalize-package-bin@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" @@ -22233,16 +21775,6 @@ p-all@^2.1.0: dependencies: p-map "^2.0.0" -p-cancelable@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" - integrity sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw== - -p-cancelable@^0.4.0: - version "0.4.1" - 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" @@ -22258,20 +21790,6 @@ p-each-series@^2.1.0: resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ== -p-event@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-event/-/p-event-1.3.0.tgz#8e6b4f4f65c72bc5b6fe28b75eda874f96a4a085" - integrity sha1-jmtPT2XHK8W2/ii3XtqHT5akoIU= - dependencies: - p-timeout "^1.1.1" - -p-event@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/p-event/-/p-event-2.3.1.tgz#596279ef169ab2c3e0cae88c1cfbb08079993ef6" - integrity sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA== - dependencies: - p-timeout "^2.0.1" - p-event@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.1.0.tgz#e92bb866d7e8e5b732293b1c8269d38e9982bf8e" @@ -22291,11 +21809,6 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-is-promise@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" - integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= - p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -22345,13 +21858,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map-series@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-1.0.0.tgz#bf98fe575705658a9e1351befb85ae4c1f07bdca" - integrity sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco= - dependencies: - p-reduce "^1.0.0" - p-map@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" @@ -22371,11 +21877,6 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -p-reduce@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" - integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= - p-retry@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" @@ -22391,13 +21892,6 @@ p-retry@^4.2.0: "@types/retry" "^0.12.0" retry "^0.12.0" -p-timeout@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386" - integrity sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y= - dependencies: - p-finally "^1.0.0" - p-timeout@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" @@ -22883,7 +22377,7 @@ picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== -pify@^2.0.0, pify@^2.2.0, pify@^2.3.0: +pify@^2.0.0, pify@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= @@ -23552,7 +23046,7 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prepend-http@^1.0.0, prepend-http@^1.0.1: +prepend-http@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= @@ -23995,15 +23489,6 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -query-string@^5.0.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" - integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== - dependencies: - decode-uri-component "^0.2.0" - object-assign "^4.1.0" - strict-uri-encode "^1.0.0" - query-string@^6.13.2: version "6.13.2" resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.2.tgz#3585aa9412c957cbd358fd5eaca7466f05586dda" @@ -25016,7 +24501,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", "readable-stream@2 || 3", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.3, readable-stream@~2.3.6: +"readable-stream@1 || 2", "readable-stream@2 || 3", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.3, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -25814,7 +25299,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= @@ -26221,13 +25706,6 @@ seedrandom@^3.0.5: resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== -seek-bzip@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc" - integrity sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w= - dependencies: - commander "~2.8.1" - select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -26787,20 +26265,6 @@ sonic-boom@^2.1.0: dependencies: atomic-sleep "^1.0.0" -sort-keys-length@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" - integrity sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg= - dependencies: - sort-keys "^1.0.0" - -sort-keys@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" - integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= - dependencies: - is-plain-obj "^1.0.0" - sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" @@ -27349,11 +26813,6 @@ stream-to-async-iterator@^0.2.0: resolved "https://registry.yarnpkg.com/stream-to-async-iterator/-/stream-to-async-iterator-0.2.0.tgz#bef5c885e9524f98b2fa5effecc357bd58483780" integrity sha1-vvXIhelST5iy+l7/7MNXvVhIN4A= -strict-uri-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" - integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= - strict-uri-encode@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" @@ -27573,23 +27032,11 @@ strip-bom@^4.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== -strip-dirs@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" - integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== - dependencies: - is-natural-number "^4.0.1" - strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= -strip-eof@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-2.0.0.tgz#2e3f3c5145d02de826eafb23e65b2faf675448b4" - integrity sha512-zLsJC+5P5hGu4Zmoq6I4uo6bTf1Nx6Z/vnZedxwnrcfkc38Vz6UiuqGOtS9bewFaoTCDErpqkV7v02htp9KEow== - strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" @@ -27612,13 +27059,6 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -strip-outer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.0.tgz#aac0ba60d2e90c5d4f275fd8869fd9a2d310ffb8" - integrity sha1-qsC6YNLpDF1PJ1/Yhp/ZotMQ/7g= - dependencies: - escape-string-regexp "^1.0.2" - strong-log-transformer@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" @@ -28056,19 +27496,6 @@ tar-fs@^2.1.0: pump "^3.0.0" tar-stream "^2.0.0" -tar-stream@^1.5.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" - integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== - dependencies: - bl "^1.0.0" - buffer-alloc "^1.2.0" - end-of-stream "^1.0.0" - fs-constants "^1.0.0" - readable-stream "^2.3.0" - to-buffer "^1.1.1" - xtend "^4.0.0" - tar-stream@^2.0.0: version "2.1.3" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41" @@ -28168,27 +27595,6 @@ temp-dir@^1.0.0: resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= -temp-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" - integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== - -tempfile@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-2.0.0.tgz#6b0446856a9b1114d1856ffcbe509cccb0977265" - integrity sha1-awRGhWqbERTRhW/8vlCczLCXcmU= - dependencies: - temp-dir "^1.0.0" - uuid "^3.0.1" - -tempfile@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-3.0.0.tgz#5376a3492de7c54150d0cc0612c3f00e2cdaf76c" - integrity sha512-uNFCg478XovRi85iD42egu+eSFUmmka750Jy7L5tfHI5hQKKtbPnxaSaXAbBqCDYrw3wx4tXjKwci4/QmsZJxw== - dependencies: - temp-dir "^2.0.0" - uuid "^3.3.2" - tempy@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.3.0.tgz#6f6c5b295695a16130996ad5ab01a8bd726e8bf8" @@ -28394,11 +27800,6 @@ timed-out@^2.0.0: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" integrity sha1-84sK6B03R9YoAB9B2vxlKs5nHAo= -timed-out@^4.0.0, timed-out@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" - integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= - timers-browserify@^1.0.1: version "1.4.2" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" @@ -28511,11 +27912,6 @@ to-arraybuffer@^1.0.0: resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= -to-buffer@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" - integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== - to-camel-case@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/to-camel-case/-/to-camel-case-1.0.0.tgz#1a56054b2f9d696298ce66a60897322b6f423e46" @@ -28701,13 +28097,6 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== -trim-repeated@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" - integrity sha1-42RqLqTokTEr9+rObPsFOAvAHCE= - dependencies: - escape-string-regexp "^1.0.2" - trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" @@ -29059,7 +28448,7 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" -unbzip2-stream@^1.0.9, unbzip2-stream@^1.3.3: +unbzip2-stream@^1.3.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== @@ -29549,13 +28938,6 @@ url-loader@^4.0.0: mime-types "^2.1.26" schema-utils "^2.6.5" -url-parse-lax@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" - integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= - dependencies: - prepend-http "^1.0.1" - url-parse-lax@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" @@ -29576,11 +28958,6 @@ url-template@^2.0.8: resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE= -url-to-options@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" - integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= - url@^0.11.0, url@~0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -29733,7 +29110,7 @@ uuid@^2.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho= -uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0: +uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -31205,7 +30582,7 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" -yauzl@^2.10.0, yauzl@^2.4.2: +yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=