diff --git a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report_security_solution.md
similarity index 91%
rename from .github/ISSUE_TEMPLATE/security_solution_bug_report.md
rename to .github/ISSUE_TEMPLATE/Bug_report_security_solution.md
index bd7d57c72ea569..059e1d267c2862 100644
--- a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md
+++ b/.github/ISSUE_TEMPLATE/Bug_report_security_solution.md
@@ -2,7 +2,7 @@
name: Bug report for Security Solution
about: Help us identify bugs in Elastic Security, SIEM, and Endpoint so we can fix them!
title: '[Security Solution]'
-labels: Team: SecuritySolution
+labels: 'Team: SecuritySolution'
---
**Describe the bug:**
diff --git a/docs/api/dashboard/export-dashboard.asciidoc b/docs/api/dashboard/export-dashboard.asciidoc
index 2099fb599ba67c..d33b9603fae73c 100644
--- a/docs/api/dashboard/export-dashboard.asciidoc
+++ b/docs/api/dashboard/export-dashboard.asciidoc
@@ -11,6 +11,8 @@ experimental[] Export dashboards and corresponding saved objects.
`GET :/api/kibana/dashboards/export`
+`GET :/s//api/kibana/dashboards/export`
+
[[dashboard-api-export-params]]
==== Query parameters
diff --git a/docs/api/dashboard/import-dashboard.asciidoc b/docs/api/dashboard/import-dashboard.asciidoc
index 56bd4abbc80236..5d1fab41a2a140 100644
--- a/docs/api/dashboard/import-dashboard.asciidoc
+++ b/docs/api/dashboard/import-dashboard.asciidoc
@@ -11,6 +11,8 @@ experimental[] Import dashboards and corresponding saved objects.
`POST :/api/kibana/dashboards/import`
+`POST :/s//api/kibana/dashboards/import`
+
[[dashboard-api-import-params]]
==== Query parameters
diff --git a/docs/api/features.asciidoc b/docs/api/features.asciidoc
index 57a87ff6342f9a..dad3ef75c81176 100644
--- a/docs/api/features.asciidoc
+++ b/docs/api/features.asciidoc
@@ -28,8 +28,6 @@ The API returns the following:
{
"id": "discover",
"name": "Discover",
- "icon": "discoverApp",
- "navLinkId": "discover",
"app": [
"kibana"
],
@@ -73,8 +71,6 @@ The API returns the following:
{
"id": "visualize",
"name": "Visualize",
- "icon": "visualizeApp",
- "navLinkId": "visualize",
"app": [
"kibana"
],
@@ -120,8 +116,6 @@ The API returns the following:
{
"id": "dashboard",
"name": "Dashboard",
- "icon": "dashboardApp",
- "navLinkId": "dashboards",
"app": [
"kibana"
],
@@ -172,8 +166,6 @@ The API returns the following:
{
"id": "dev_tools",
"name": "Dev Tools",
- "icon": "devToolsApp",
- "navLinkId": "dev_tools",
"app": [
"kibana"
],
diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc
index bc5e1ccc1dd55e..7bdfe80b421777 100644
--- a/docs/apm/apm-alerts.asciidoc
+++ b/docs/apm/apm-alerts.asciidoc
@@ -18,12 +18,22 @@ image::apm/images/apm-alert.png[Create an alert in the APM app]
For a walkthrough of the alert flyout panel, including detailed information on each configurable property,
see Kibana's <>.
-The APM app supports two different types of threshold alerts: transaction duration, and error rate.
-Below, we'll create one of each.
+The APM app supports four different types of alerts:
+
+* Transaction duration anomaly:
+alerts when the service's transaction duration reaches a certain anomaly score
+* Transaction duration threshold:
+alerts when the service's transaction duration exceeds a given time limit over a given time frame
+* Transaction error rate threshold:
+alerts when the service's transaction error rate is above the selected rate over a given time frame
+* Error count threshold:
+alerts when service exceeds a selected number of errors over a given time frame
+
+Below, we'll walk through the creation of two of these alerts.
[float]
[[apm-create-transaction-alert]]
-=== Create a transaction duration alert
+=== Example: create a transaction duration alert
Transaction duration alerts trigger when the duration of a specific transaction type in a service exceeds a defined threshold.
This guide will create an alert for the `opbeans-java` service based on the following criteria:
@@ -57,9 +67,9 @@ Enter a name for the connector,
and paste the webhook URL.
See Slack's webhook documentation if you need to create one.
-Add a message body in markdown format.
+A default message is provided as a starting point for your alert.
You can use the https://mustache.github.io/[Mustache] template syntax, i.e., `{{variable}}`
-to pass alert values at the time a condition is detected to an action.
+to pass additional alert values at the time a condition is detected to an action.
A list of available variables can be accessed by selecting the
**add variable** button image:apm/images/add-variable.png[add variable button].
@@ -67,7 +77,7 @@ Select **Save**. The alert has been created and is now active!
[float]
[[apm-create-error-alert]]
-=== Create an error rate alert
+=== Example: create an error rate alert
Error rate alerts trigger when the number of errors in a service exceeds a defined threshold.
This guide creates an alert for the `opbeans-python` service based on the following criteria:
@@ -94,9 +104,9 @@ Based on the alert criteria, define the following alert details:
Select the **Email** action type and click **Create a connector**.
Fill out the required details: sender, host, port, etc., and click **save**.
-Add a message body in markdown format.
+A default message is provided as a starting point for your alert.
You can use the https://mustache.github.io/[Mustache] template syntax, i.e., `{{variable}}`
-to pass alert values at the time a condition is detected to an action.
+to pass additional alert values at the time a condition is detected to an action.
A list of available variables can be accessed by selecting the
**add variable** button image:apm/images/add-variable.png[add variable button].
diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc
index d53adb439f0c8e..c405ea10ade3d2 100644
--- a/docs/apm/filters.asciidoc
+++ b/docs/apm/filters.asciidoc
@@ -69,7 +69,7 @@ the host filter will still be applied.
These filters are very useful for quickly and easily removing noise from your data.
With just a click, you can filter your transactions by the transaction result,
-host, container ID, and more.
+host, container ID, Kubernetes pod, and more.
[role="screenshot"]
image::apm/images/local-filter.png[Local filters available in the APM app in Kibana]
\ No newline at end of file
diff --git a/docs/apm/images/apm-alert.png b/docs/apm/images/apm-alert.png
index 350704d8969ae6..c68b36f522bfc0 100644
Binary files a/docs/apm/images/apm-alert.png and b/docs/apm/images/apm-alert.png differ
diff --git a/docs/apm/images/apm-distributed-tracing.png b/docs/apm/images/apm-distributed-tracing.png
index e9c6713361c731..0dbffa591d43aa 100644
Binary files a/docs/apm/images/apm-distributed-tracing.png and b/docs/apm/images/apm-distributed-tracing.png differ
diff --git a/docs/apm/images/apm-error-group.png b/docs/apm/images/apm-error-group.png
index ecdf9c20cf4aa3..359bdc6b704e94 100644
Binary files a/docs/apm/images/apm-error-group.png and b/docs/apm/images/apm-error-group.png differ
diff --git a/docs/apm/images/apm-errors-overview.png b/docs/apm/images/apm-errors-overview.png
index 90f16b81e9f50e..969a1f19f9f436 100644
Binary files a/docs/apm/images/apm-errors-overview.png and b/docs/apm/images/apm-errors-overview.png differ
diff --git a/docs/apm/images/apm-geo-ui.png b/docs/apm/images/apm-geo-ui.png
index a767ed7e08e0cf..3757127bad9c09 100644
Binary files a/docs/apm/images/apm-geo-ui.png and b/docs/apm/images/apm-geo-ui.png differ
diff --git a/docs/apm/images/apm-metrics.png b/docs/apm/images/apm-metrics.png
index 60383ef428f2a2..ffe5ffc7e1d83f 100644
Binary files a/docs/apm/images/apm-metrics.png and b/docs/apm/images/apm-metrics.png differ
diff --git a/docs/apm/images/apm-query-bar.png b/docs/apm/images/apm-query-bar.png
index 313ee7d4b8fc83..90955fb61016da 100644
Binary files a/docs/apm/images/apm-query-bar.png and b/docs/apm/images/apm-query-bar.png differ
diff --git a/docs/apm/images/apm-service-map-anomaly.png b/docs/apm/images/apm-service-map-anomaly.png
index b661e8f09d1a12..cd59f86690666a 100644
Binary files a/docs/apm/images/apm-service-map-anomaly.png and b/docs/apm/images/apm-service-map-anomaly.png differ
diff --git a/docs/apm/images/apm-services-overview.png b/docs/apm/images/apm-services-overview.png
index 48236522ddfbb8..85d14cc7dfc6e6 100644
Binary files a/docs/apm/images/apm-services-overview.png and b/docs/apm/images/apm-services-overview.png differ
diff --git a/docs/apm/images/apm-settings.png b/docs/apm/images/apm-settings.png
index 4eaef9ec15ac53..14cf32877b7201 100644
Binary files a/docs/apm/images/apm-settings.png and b/docs/apm/images/apm-settings.png differ
diff --git a/docs/apm/images/apm-traces.png b/docs/apm/images/apm-traces.png
index 6219be5b6d6e49..bf1f7e783bb110 100644
Binary files a/docs/apm/images/apm-traces.png and b/docs/apm/images/apm-traces.png differ
diff --git a/docs/apm/images/apm-transaction-response-dist.png b/docs/apm/images/apm-transaction-response-dist.png
index ecf5a4af2c25df..1d268bbaac465c 100644
Binary files a/docs/apm/images/apm-transaction-response-dist.png and b/docs/apm/images/apm-transaction-response-dist.png differ
diff --git a/docs/apm/images/apm-transaction-sample.png b/docs/apm/images/apm-transaction-sample.png
index 73668b094f9cf4..bfdb6a5abe65bc 100644
Binary files a/docs/apm/images/apm-transaction-sample.png and b/docs/apm/images/apm-transaction-sample.png differ
diff --git a/docs/apm/images/apm-transactions-overview.png b/docs/apm/images/apm-transactions-overview.png
index b3b6ca22c4f635..53d7637b18647c 100644
Binary files a/docs/apm/images/apm-transactions-overview.png and b/docs/apm/images/apm-transactions-overview.png differ
diff --git a/docs/apm/images/example-metadata.png b/docs/apm/images/example-metadata.png
index 0e35f906917235..2a5bda7f088f69 100644
Binary files a/docs/apm/images/example-metadata.png and b/docs/apm/images/example-metadata.png differ
diff --git a/docs/apm/images/jvm-metrics-overview.png b/docs/apm/images/jvm-metrics-overview.png
index 9c8ba4a12a2629..586836c6cfe3eb 100644
Binary files a/docs/apm/images/jvm-metrics-overview.png and b/docs/apm/images/jvm-metrics-overview.png differ
diff --git a/docs/apm/images/jvm-metrics.png b/docs/apm/images/jvm-metrics.png
index 1720e1370ff901..52a1ca5bea8d87 100644
Binary files a/docs/apm/images/jvm-metrics.png and b/docs/apm/images/jvm-metrics.png differ
diff --git a/docs/apm/images/local-filter.png b/docs/apm/images/local-filter.png
index faac5c143a7d83..8657e39f430aa0 100644
Binary files a/docs/apm/images/local-filter.png and b/docs/apm/images/local-filter.png differ
diff --git a/docs/apm/images/service-maps-java.png b/docs/apm/images/service-maps-java.png
index e1a42f4c76e12f..b3726bdc00ab68 100644
Binary files a/docs/apm/images/service-maps-java.png and b/docs/apm/images/service-maps-java.png differ
diff --git a/docs/apm/images/service-maps.png b/docs/apm/images/service-maps.png
index 078fabcfa2879c..878a31adc69cae 100644
Binary files a/docs/apm/images/service-maps.png and b/docs/apm/images/service-maps.png differ
diff --git a/docs/apm/images/service-quick-health.png b/docs/apm/images/service-quick-health.png
new file mode 100644
index 00000000000000..aab13325130797
Binary files /dev/null and b/docs/apm/images/service-quick-health.png differ
diff --git a/docs/apm/images/specific-transaction.png b/docs/apm/images/specific-transaction.png
index 9911dbd879f412..52073bf76520a4 100644
Binary files a/docs/apm/images/specific-transaction.png and b/docs/apm/images/specific-transaction.png differ
diff --git a/docs/apm/machine-learning.asciidoc b/docs/apm/machine-learning.asciidoc
index db2a1ef6e2da08..b31d717a6932e8 100644
--- a/docs/apm/machine-learning.asciidoc
+++ b/docs/apm/machine-learning.asciidoc
@@ -14,7 +14,12 @@ Machine learning jobs are created per environment, and are based on a service's
Because jobs are created at the environment level,
you can add new services to your existing environments without the need for additional machine learning jobs.
-After a machine learning job is created, results are shown in two places:
+Results from machine learning jobs are shown in multiple places throughout the APM app:
+
+* The **Services overview** provides a quick-glance view of the general health of all of your services.
++
+[role="screenshot"]
+image::apm/images/service-quick-health.png[Example view of anomaly scores on response times in the APM app]
* The transaction duration chart will show the expected bounds and add an annotation when the anomaly score is 75 or above.
+
diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc
index d629a95073a745..d44c4ff6caa5cb 100644
--- a/docs/apm/service-maps.asciidoc
+++ b/docs/apm/service-maps.asciidoc
@@ -33,7 +33,7 @@ distributed tracing will not work, and the connection will not be drawn on the m
Select the **Service Map** tab to get started.
By default, all instrumented services and connections are shown.
Whether you're onboarding a new engineer, or just trying to grasp the big picture,
-click around, zoom in and out, and begin to visualize how your services are connected.
+drag things around, zoom in and out, and begin to visualize how your services are connected.
If there's a specific service that interests you, select that service to highlight its connections.
Clicking **Focus map** will refocus the map on that specific service and lock the connection highlighting.
diff --git a/docs/apm/services.asciidoc b/docs/apm/services.asciidoc
index 395e23c3793064..2bf2e35c21cd82 100644
--- a/docs/apm/services.asciidoc
+++ b/docs/apm/services.asciidoc
@@ -2,8 +2,13 @@
[[services]]
=== Services overview
-The *Services* overview gives you quick insights into the health and general performance of all of your instrumented services.
-Services are sorted by the `service.name` configured in each of the {apm-agents-ref}[APM agents] you’ve installed.
+The *Services* overview page provides a quick, high-level overview of the health and general
+performance of all instrumented services.
+
+To help surface potential issues, services are sorted by their health status:
+**critical** > **warning** > **healthy** > **unknown**.
+Health status is powered by machine learning and requires anomaly detection to be enabled.
+Learn more in <>.
[role="screenshot"]
-image::apm/images/apm-services-overview.png[Example view of services table the APM app in Kibana]
\ No newline at end of file
+image::apm/images/apm-services-overview.png[Example view of services table the APM app in Kibana]
diff --git a/docs/apm/spans.asciidoc b/docs/apm/spans.asciidoc
index c35fb115d2db44..7f29b1f003f1cf 100644
--- a/docs/apm/spans.asciidoc
+++ b/docs/apm/spans.asciidoc
@@ -3,7 +3,7 @@
=== Trace sample timeline
The trace sample timeline visualization is a bird's-eye view of what your application was doing while it was trying to respond to a request.
-This makes it useful for visualizing where the selected transaction spent most of its time.
+This makes it useful for visualizing where a selected transaction spent most of its time.
[role="screenshot"]
image::apm/images/apm-transaction-sample.png[Example of distributed trace colors in the APM app in Kibana]
@@ -43,9 +43,12 @@ this makes finding possible bottlenecks throughout your application much easier
image::apm/images/apm-distributed-tracing.png[Example view of the distributed tracing in APM app in Kibana]
Don't forget; by definition, a distributed trace includes more than one transaction.
-When viewing these distributed traces in the timeline waterfall, you'll see this image:apm/images/transaction-icon.png[APM icon] icon,
+When viewing distributed traces in the timeline waterfall,
+you'll see this icon: image:apm/images/transaction-icon.png[APM icon],
which indicates the next transaction in the trace.
-These transactions can be expanded and viewed in detail by clicking on them.
+For easier problem isolation, transactions can be collapsed in the waterfall by clicking
+the icon to the left of the transactions.
+Transactions can also be expanded and viewed in detail by clicking on them.
After exploring these traces,
you can return to the full trace by clicking *View full trace*.
diff --git a/docs/apm/traces.asciidoc b/docs/apm/traces.asciidoc
index 52b4b618de4663..3bafebd7331597 100644
--- a/docs/apm/traces.asciidoc
+++ b/docs/apm/traces.asciidoc
@@ -7,7 +7,8 @@ and which services were part of it.
In addition to the Traces overview, you can view your application traces in the <>.
The *Traces* overview displays the entry transaction for all traces in your application.
-If you're using <>, this view is key to finding the critical paths within your application.
+If you're using <>,
+this view is key to finding the critical paths within your application.
Transactions with the same name are grouped together and only shown once in this table.
By default, transactions are sorted by _Impact_.
diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc
index 84ab6b2a58579d..fef98a86de1d01 100644
--- a/docs/apm/transactions.asciidoc
+++ b/docs/apm/transactions.asciidoc
@@ -10,17 +10,8 @@ Selecting a <> brings you to the *transactions* overview.
[role="screenshot"]
image::apm/images/apm-transactions-overview.png[Example view of transactions table in the APM app in Kibana]
-The *time spent by span type*, *transaction duration*, and *requests per minute* chart display information on all transactions associated with the selected service:
-
-*Time spent by span type*::
-Visualize where your application is spending most of its time.
-For example, is your app spending time in external calls, database processing, or application code execution?
-+
-The time a transaction took to complete is also recorded and displayed on the chart under the "app" label.
-"app" indicates that something was happening within the application, but we're not sure exactly what.
-This could be a sign that the agent does not have auto-instrumentation for whatever was happening during that time.
-+
-It's important to note that if you have asynchronous spans, the sum of all span times may exceed the duration of the transaction.
+The *transaction duration*, *transactions per minute*, *transaction error rate*, and *time spent by span type*
+charts display information on all transactions associated with the selected service:
*Transaction duration*::
Response times for this service, broken down into average, 95th, and 99th percentile.
@@ -28,11 +19,26 @@ If there's a weird spike that you'd like to investigate,
you can simply zoom in on the graph - this will adjust the specific time range,
and all of the data on the page will update accordingly.
-*Requests per minute*::
+*Transactions per minute*::
Visualize response codes: `2xx`, `3xx`, `4xx`, etc.,
and is useful for determining if you're serving more of one code than you typically do.
Like in the Transaction duration graph, you can zoom in on anomalies to further investigate them.
+*Transaction error rate*::
+Visualize the total number of transactions with errors divided by the total number of transactions.
+Any unexpected increases, decreases, or irregular patterns can be investigated further
+with the <>.
+
+*Time spent by span type*::
+Visualize where your application is spending most of its time.
+For example, is your app spending time in external calls, database processing, or application code execution?
++
+The time a transaction took to complete is also recorded and displayed on the chart under the "app" label.
+"app" indicates that something was happening within the application, but we're not sure exactly what.
+This could be a sign that the agent does not have auto-instrumentation for whatever was happening during that time.
++
+It's important to note that if you have asynchronous spans, the sum of all span times may exceed the duration of the transaction.
+
[[transactions-table]]
==== Transactions table
@@ -61,42 +67,45 @@ refer to the documentation for each {apm-agents-ref}[APM Agent] you've implement
==== RUM Transaction overview
The transaction overview page is customized for the JavaScript RUM Agent.
-This page highlights things like *page load times*, *transactions per minute*, and even the *average page load duration distribution by country*.
+Specifically, the page highlights *page load times* for your service:
[role="screenshot"]
image::apm/images/apm-geo-ui.png[average page load duration distribution]
-This data is available due to the geo-ip and user agent pipelines being enabled by default,
-which allows for the capture of geo-location and user agent data.
-These visualizations make it easy for you to visualize performance information about your
-end-users' experience based on their location.
+Additional RUM goodies, like core vitals, and visitor breakdown by browser, location, and device,
+are available in the Observability User Experience tab.
+// To do
+// Add link to the Observability UE docs when complete
[[transaction-details]]
==== Transaction details
Selecting a transaction group will bring you to the *transaction* details.
-Transaction details include a high-level overview of the time spent by span type,
-transaction group duration, requests per minute, and transaction group duration distribution.
-It's important to note that all of these graphs show data from every transaction within the selected transaction group.
+This page is visually similar to the transaction overview, but it shows data from all transactions within
+the selected transaction group.
[role="screenshot"]
image::apm/images/apm-transaction-response-dist.png[Example view of response time distribution]
Up to ten sampled transactions are also displayed.
-These sampled transactions are based on your selection in the *Transactions duration distribution*.
-You can update the sampled transactions by selecting a new _bucket_ in the transactions duration distribution graph.
-The number of requests per bucket is displayed when hovering over the graph, and the selected bucket is highlighted to stand out.
+These sampled transactions are based on the _bucket_ selection in the *Transactions duration distribution* chart.
+You can update the sampled transactions by selecting a new _bucket_.
+The number of requests per bucket is displayed when hovering over the graph,
+and the selected bucket is highlighted to stand out.
+
+The screenshot below shows a typical distribution, and indicates most of our requests were served quickly--awesome!
+It's the requests on the right, the ones taking longer than average, that we probably want to focus on.
[role="screenshot"]
image::apm/images/apm-transaction-duration-dist.png[Example view of transactions duration distribution graph]
-This graph shows a typical distribution, and indicates most of our requests were served quickly--awesome!
-It's the requests on the right, the ones taking longer than average, that we probably want to focus on.
-
-When you select one of these buckets,
+When you select a bucket,
you're presented with up to ten trace samples.
-Each sample has a trace timeline waterfall that shows what a typical request in that bucket was doing.
-By investigating this timeline waterfall, we can hopefully determine _why_ this request was slow and then implement a fix.
+Each sample has a trace timeline waterfall that shows how a typical request in that bucket executed.
+This waterfall is useful for understanding the parent/child hierarchy of transactions and spans,
+and ultimately determining _why_ a request was slow.
+For large waterfalls, expand problematic transactions and collapse well-performing ones
+for easier problem isolation and troubleshooting.
[role="screenshot"]
image::apm/images/apm-transaction-sample.png[Example view of transactions sample]
diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc
index b4c9c6a4ec39e4..6c52c021fc0fcc 100644
--- a/docs/apm/troubleshooting.asciidoc
+++ b/docs/apm/troubleshooting.asciidoc
@@ -14,6 +14,7 @@ Also, check out the https://discuss.elastic.co/c/apm[APM discussion forum].
* <>
* <>
* <>
+* <>
[float]
[[no-apm-data-found]]
@@ -180,3 +181,19 @@ setup.template.append_fields:
type: object
dynamic: true
----
+
+[float]
+[[service-map-rum-connections]]
+=== Service maps: no connection between client and server
+
+If the service map is not showing an expected connection between the client and server,
+it's likely because you haven't configured
+{apm-agent-rum}/configuration.html#distributed-tracing-origins[`distributedTracingOrigins`].
+
+
+This setting is necessary, for example, for cross-origin requests.
+If you have a basic web application that provides data via an API on `localhost:4000`,
+and serves HTML from `localhost:4001`, you'd need to set `distributedTracingOrigins: ['https://localhost:4000']`
+to ensure the origin is monitored as a part of distributed tracing.
+In other words, `distributedTracingOrigins` is consulted prior to the agent adding the
+distributed tracing `traceparent` header to each request.
diff --git a/docs/developer/architecture/security/feature-registration.asciidoc b/docs/developer/architecture/security/feature-registration.asciidoc
index b27e457940d933..8c80c2e5f2ffb4 100644
--- a/docs/developer/architecture/security/feature-registration.asciidoc
+++ b/docs/developer/architecture/security/feature-registration.asciidoc
@@ -59,15 +59,6 @@ of features within the management screens.
|See <>
|The set of subfeatures that enables finer access control than the `all` and `read` feature privileges. These options are only available in the Gold subscription level and higher.
-|`icon`
-|`string`
-|"discoverApp"
-|An https://elastic.github.io/eui/#/display/icons[EUI Icon] to use for this feature.
-
-|`navLinkId`
-|`string`
-|"sample_app"
-|The ID of the navigation link associated with your feature.
|===
==== Privilege definition
@@ -100,8 +91,6 @@ public setup(core, { features }) {
features.registerKibanaFeature({
id: 'canvas',
name: 'Canvas',
- icon: 'canvasApp',
- navLinkId: 'canvas',
category: DEFAULT_APP_CATEGORIES.kibana,
app: ['canvas', 'kibana'],
catalogue: ['canvas'],
@@ -160,8 +149,6 @@ public setup(core, { features }) {
name: i18n.translate('xpack.features.devToolsFeatureName', {
defaultMessage: 'Dev Tools',
}),
- icon: 'devToolsApp',
- navLinkId: 'dev_tools',
category: DEFAULT_APP_CATEGORIES.management,
app: ['kibana'],
catalogue: ['console', 'searchprofiler', 'grokdebugger'],
@@ -223,8 +210,6 @@ public setup(core, { features }) {
defaultMessage: 'Discover',
}),
order: 100,
- icon: 'discoverApp',
- navLinkId: 'discover',
category: DEFAULT_APP_CATEGORIES.kibana,
app: ['kibana'],
catalogue: ['discover'],
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md
index c5e01715534d1c..ad762cae489c87 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md
@@ -43,4 +43,5 @@ export declare enum ES_FIELD_TYPES
| STRING | "string"
| |
| TEXT | "text"
| |
| TOKEN\_COUNT | "token_count"
| |
+| UNSIGNED\_LONG | "unsigned_long"
| |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md
new file mode 100644
index 00000000000000..26276a809a6131
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [deleteFieldFormat](./kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md)
+
+## IndexPattern.deleteFieldFormat property
+
+Signature:
+
+```typescript
+deleteFieldFormat: (fieldName: string) => void;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md
new file mode 100644
index 00000000000000..0dd171108b20b3
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md
@@ -0,0 +1,24 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [getFormatterForFieldNoDefault](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md)
+
+## IndexPattern.getFormatterForFieldNoDefault() method
+
+Get formatter for a given field name. Return undefined if none exists
+
+Signature:
+
+```typescript
+getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| fieldname | string
| |
+
+Returns:
+
+`FieldFormat | undefined`
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md
index c07041470d102e..7e3192481dfffa 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md
@@ -20,6 +20,7 @@ export declare class IndexPattern implements IIndexPattern
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
+| [deleteFieldFormat](./kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md) | | (fieldName: string) => void
| |
| [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md) | | Record<string, any>
| |
| [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
}
| |
| [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any>
| |
@@ -30,6 +31,7 @@ export declare class IndexPattern implements IIndexPattern
| [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) | | string | undefined
| |
| [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[]
| |
| [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md) | | () => void
| Reset last saved saved object fields. used after saving |
+| [setFieldFormat](./kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md) | | (fieldName: string, format: SerializedFieldFormat) => void
| |
| [sourceFilters](./kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md) | | SourceFilter[]
| |
| [timeFieldName](./kibana-plugin-plugins-data-public.indexpattern.timefieldname.md) | | string | undefined
| |
| [title](./kibana-plugin-plugins-data-public.indexpattern.title.md) | | string
| |
@@ -47,6 +49,7 @@ export declare class IndexPattern implements IIndexPattern
| [getComputedFields()](./kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md) | | |
| [getFieldByName(name)](./kibana-plugin-plugins-data-public.indexpattern.getfieldbyname.md) | | |
| [getFormatterForField(field)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter |
+| [getFormatterForFieldNoDefault(fieldname)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md) | | Get formatter for a given field name. Return undefined if none exists |
| [getNonScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getnonscriptedfields.md) | | |
| [getScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md) | | |
| [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md
new file mode 100644
index 00000000000000..9774fc8c7308cf
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [setFieldFormat](./kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md)
+
+## IndexPattern.setFieldFormat property
+
+Signature:
+
+```typescript
+setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md
new file mode 100644
index 00000000000000..af4115e4c4e094
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [fieldFormats](./kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md)
+
+## IndexPatternSpec.fieldFormats property
+
+Signature:
+
+```typescript
+fieldFormats?: Record;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md
index 74c4df126e1bf6..f3b692209ca671 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md
@@ -14,6 +14,7 @@ export interface IndexPatternSpec
| Property | Type | Description |
| --- | --- | --- |
+| [fieldFormats](./kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md) | Record<string, SerializedFieldFormat>
| |
| [fields](./kibana-plugin-plugins-data-public.indexpatternspec.fields.md) | IndexPatternFieldMap
| |
| [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) | string
| |
| [intervalName](./kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md) | string
| |
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md
index d071955f4f522d..545b7b9d27e10d 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md
@@ -43,4 +43,5 @@ export declare enum ES_FIELD_TYPES
| STRING | "string"
| |
| TEXT | "text"
| |
| TOKEN\_COUNT | "token_count"
| |
+| UNSIGNED\_LONG | "unsigned_long"
| |
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md
new file mode 100644
index 00000000000000..4bfda565274741
--- /dev/null
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [deleteFieldFormat](./kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md)
+
+## IndexPattern.deleteFieldFormat property
+
+Signature:
+
+```typescript
+deleteFieldFormat: (fieldName: string) => void;
+```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md
new file mode 100644
index 00000000000000..77cc879e2f2f2f
--- /dev/null
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md
@@ -0,0 +1,24 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getFormatterForFieldNoDefault](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md)
+
+## IndexPattern.getFormatterForFieldNoDefault() method
+
+Get formatter for a given field name. Return undefined if none exists
+
+Signature:
+
+```typescript
+getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| fieldname | string
| |
+
+Returns:
+
+`FieldFormat | undefined`
+
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md
index 603864234d34bb..2e15c8d3867eca 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md
@@ -20,6 +20,7 @@ export declare class IndexPattern implements IIndexPattern
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
+| [deleteFieldFormat](./kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md) | | (fieldName: string) => void
| |
| [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md) | | Record<string, any>
| |
| [fields](./kibana-plugin-plugins-data-server.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
}
| |
| [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any>
| |
@@ -30,6 +31,7 @@ export declare class IndexPattern implements IIndexPattern
| [intervalName](./kibana-plugin-plugins-data-server.indexpattern.intervalname.md) | | string | undefined
| |
| [metaFields](./kibana-plugin-plugins-data-server.indexpattern.metafields.md) | | string[]
| |
| [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md) | | () => void
| Reset last saved saved object fields. used after saving |
+| [setFieldFormat](./kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md) | | (fieldName: string, format: SerializedFieldFormat) => void
| |
| [sourceFilters](./kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md) | | SourceFilter[]
| |
| [timeFieldName](./kibana-plugin-plugins-data-server.indexpattern.timefieldname.md) | | string | undefined
| |
| [title](./kibana-plugin-plugins-data-server.indexpattern.title.md) | | string
| |
@@ -47,6 +49,7 @@ export declare class IndexPattern implements IIndexPattern
| [getComputedFields()](./kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md) | | |
| [getFieldByName(name)](./kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md) | | |
| [getFormatterForField(field)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter |
+| [getFormatterForFieldNoDefault(fieldname)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md) | | Get formatter for a given field name. Return undefined if none exists |
| [getNonScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md) | | |
| [getScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md) | | |
| [getSourceFiltering()](./kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. |
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md
new file mode 100644
index 00000000000000..a8f2e726dd9b31
--- /dev/null
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [setFieldFormat](./kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md)
+
+## IndexPattern.setFieldFormat property
+
+Signature:
+
+```typescript
+setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void;
+```
diff --git a/docs/management/images/management-index-templates-mappings.png b/docs/management/images/management-index-templates-mappings.png
old mode 100755
new mode 100644
index 62321fc0e46660..beb964b348171f
Binary files a/docs/management/images/management-index-templates-mappings.png and b/docs/management/images/management-index-templates-mappings.png differ
diff --git a/docs/management/images/management-index-templates.png b/docs/management/images/management-index-templates.png
old mode 100755
new mode 100644
index 6f2564af72b5ca..07f1fb9a7add11
Binary files a/docs/management/images/management-index-templates.png and b/docs/management/images/management-index-templates.png differ
diff --git a/docs/management/images/management_index_component_template.png b/docs/management/images/management_index_component_template.png
new file mode 100644
index 00000000000000..c03029fd172f05
Binary files /dev/null and b/docs/management/images/management_index_component_template.png differ
diff --git a/docs/management/images/management_index_create_wizard.png b/docs/management/images/management_index_create_wizard.png
old mode 100755
new mode 100644
index b18c36366be94d..bff1dd4cd0e7ab
Binary files a/docs/management/images/management_index_create_wizard.png and b/docs/management/images/management_index_create_wizard.png differ
diff --git a/docs/management/images/management_index_data_stream_backing_index.png b/docs/management/images/management_index_data_stream_backing_index.png
new file mode 100644
index 00000000000000..a5c577affbbb2d
Binary files /dev/null and b/docs/management/images/management_index_data_stream_backing_index.png differ
diff --git a/docs/management/images/management_index_data_stream_stats.png b/docs/management/images/management_index_data_stream_stats.png
new file mode 100644
index 00000000000000..a67ab4a7deb320
Binary files /dev/null and b/docs/management/images/management_index_data_stream_stats.png differ
diff --git a/docs/management/images/management_index_details.png b/docs/management/images/management_index_details.png
index 77aeaba4723077..b199d13218f5ae 100644
Binary files a/docs/management/images/management_index_details.png and b/docs/management/images/management_index_details.png differ
diff --git a/docs/management/images/management_index_labels.png b/docs/management/images/management_index_labels.png
old mode 100755
new mode 100644
index 79e378e367e781..a89c32e08beff1
Binary files a/docs/management/images/management_index_labels.png and b/docs/management/images/management_index_labels.png differ
diff --git a/docs/management/managing-indices.asciidoc b/docs/management/managing-indices.asciidoc
index 24cd094c877c64..b199e076443ab2 100644
--- a/docs/management/managing-indices.asciidoc
+++ b/docs/management/managing-indices.asciidoc
@@ -2,32 +2,48 @@
[[managing-indices]]
== Index Management
-*Index Management* enables you to view index settings,
-mappings, and statistics and perform index-level operations.
-These include refreshing, flushing, clearing the cache, force merging segments,
-freezing indices, and more. Practicing good index management helps ensure
-that your data is stored in the most cost-effective way possible.
+*Index Management* features are an easy, convenient way to manage your
+{es} cluster's indices, data streams, and index templates. Practicing good index
+management ensures your data is stored correctly and in the most cost-effective
+way possible.
-*Index Management* also helps you create index templates. A template reduces
-the amount of bookkeeping when working with indices. Instead of manually
-setting up your indices, you can create them automatically from a template,
-ensuring that your settings, mappings, and aliases are consistently defined.
+[float]
+=== What you'll learn
+
+This page shows you how to use *Index Management* features to:
-To manage your indices, open the menu, then go to *Stack Management > Data > Index Management*.
+* View and edit index settings.
+* View mappings and statistics for an index.
+* Perform index-level operations, such as refreshes and freezes.
+* View and manage data streams.
+* Create index templates to automatically configure new data streams and
+indices.
+
+To manage your indices, open the menu, then click *Stack Management > Index
+Management*.
[role="screenshot"]
image::images/management_index_labels.png[Index Management UI]
-If security is enabled,
-you must have the `monitor` cluster privilege and the `view_index_metadata`
+[float]
+=== Before you start
+
+Before using this feature, you should be familiar with index management
+operations. Refer to the {ref}/indices.html[index management APIs], the
+{ref}/indices-templates.html[index template APIs], and the
+{ref}/data-streams.html[data streams documentation].
+
+[float]
+=== Required permissions
+
+The minimum required permissions to access *Index Management* are
+the `monitor` cluster privilege and the `view_index_metadata`
and `manage` index privileges to view the data.
For index templates, you must have the `manage_index_templates` cluster privilege.
See {ref}/security-privileges.html[Security privileges] for more
information.
-Before using this feature, you should be familiar with index management
-operations. Refer to the {ref}/indices.html[index management APIs]
-and the {ref}/indices-templates.html[index template APIs].
+You can add these privileges in *Stack Management > Security > Roles*.
[float]
=== View and edit indices
@@ -50,7 +66,7 @@ image::images/management_index_details.png[Index Management UI]
[float]
=== Perform index-level operations
-Use the *Manage* menu to perform index-level operations. This menu
+Use the *Manage* menu to perform index-level operations. This menu
is available in the index details view, or when you select the checkbox of one or more
indices on the overview page. The menu includes the following actions:
@@ -78,16 +94,37 @@ searchable, but queries take longer.
* *Delete index*. Permanently removes the index and all of its documents.
-* *Add lifecycle policy*. Specifies a policy for managing the lifecycle of the
+* *Add lifecycle policy*. Specifies a policy for managing the lifecycle of the
index.
+[float]
+[[manage-data-streams]]
+=== Manage data streams
+
+A {ref}/data-streams.html[data stream] lets you store time series data across
+multiple backing indices while giving you a single named resource to use in
+requests. The *Data Streams* view lists your data streams and lets you examine
+or delete them.
+
+To view more information about a data stream, such as its generation or its
+current index lifecycle policy, click the stream's name.
+
+[role="screenshot"]
+image::images/management_index_data_stream_stats.png[Data stream details]
+
+To view information about the stream's backing indices, click the number in the
+*Indices* column.
+
+[role="screenshot"]
+image::images/management_index_data_stream_backing_index.png[Backing index]
+
[float]
[[manage-index-templates]]
=== Manage index templates
An index template defines {ref}/index-modules.html#index-modules-settings[settings],
{ref}/mapping.html[mappings], and {ref}/indices-add-alias.html[aliases]
-that you can automatically apply when creating a new index. {es} applies a
+that you can automatically apply when creating a new index. {es} applies a
template to a new index based on an index pattern that matches the index name.
The *Index Templates* view lists your templates and enables you to examine, edit, clone, and
@@ -103,33 +140,56 @@ so you must create the
template before you create the indices.
[float]
-==== Example: Create an index template
+==== Try it: Create an index template
-In this example, you’ll create an index template for randomly generated log files.
+In this tutorial, you’ll create an index template for randomly generated log
+files. You'll then use the template to configure two new indices.
-Open the *Create template* wizard, and enter `logs_template` in the *Name*
-field. Set *Index pattern* to `logstash*` so the template matches any index
-with that index pattern. The merge order and version are both optional,
-and you'll leave them blank in this example.
+*Step 1. Add a name and index pattern*
+. In the *Index Templates* view, open the *Create template* wizard.
++
[role="screenshot"]
image::images/management_index_create_wizard.png[Create wizard]
-The second step in the *Create template* wizard allows you to define index settings.
-These settings are optional, and this example skips this step.
+. In the *Name* field, enter `my-index-template`.
-The logs data set requires a
-mapping to label the latitude and longitude pairs as geographic locations
-by applying the geo_point type. In the third step of the wizard, define this mapping
-under the *Mapped fields* tab as follows:
+. Set *Index pattern* to `my-index-*` so the template matches any index
+with that index pattern.
+. Leave *Data Stream*, *Priority*, *Version*, and *_meta field* as-is or blank.
+
+. Click *Next*.
+
+*Step 2. Add settings, mappings, and index aliases*
+
+. Add component templates to your index template.
++
+{ref}/indices-component-template.html[Component templates] are pre-configured
+sets of mappings, index settings, and index aliases you can reuse across
+multiple index templates. Badges indicate whether a component template contains
+mappings (*M*), index settings (*S*), index aliases (*A*), or a combination of
+the three.
++
+Component templates are optional. For this tutorial, do not add any component
+templates.
++
[role="screenshot"]
-image::images/management-index-templates-mappings.png[Mapped fields page]
+image::images/management_index_component_template.png[Component templates page]
-Alternatively, you can click the *Load JSON* link and define the mapping as JSON:
+. Define index settings. These are optional. For this tutorial, leave this
+section blank.
+. Define a mapping that contains an object field named `geo` with a child
+geo-point field named `coordinates`:
++
+[role="screenshot"]
+image::images/management-index-templates-mappings.png[Mapped fields page]
++
+Alternatively, you can click the *Load JSON* link and define the mapping as JSON:
++
[source,js]
-----------------------------------
+----
{
"properties": {
"geo": {
@@ -141,28 +201,33 @@ Alternatively, you can click the *Load JSON* link and define the mapping as JSON
}
}
}
-----------------------------------
-
+----
++
You can create additional mapping configurations in the *Dynamic templates* and
-*Advanced options* tabs. No additional mappings are required for this example.
-
-In the fourth step, define an alias named `logstash`.
+*Advanced options* tabs. No additional mappings are required for this tutorial.
+. Define an index alias named `my-index`:
++
[source,js]
-----------------------------------
+----
{
- "logstash": {}
+ "my-index": {}
}
-----------------------------------
+----
+
+. On the review page, check the summary. If everything looks right, click
+*Create template*.
-A summary of the template is in step 5. If everything looks right, click *Create template*.
+*Step 3. Create new indices*
-At this point, you’re ready to use the {es} index API to load the logs data.
-In the {kib} *Console*, index two documents:
+You’re now ready to load the logs data and create new indices using your index
+template.
+. In the {kib} *Console*, index the following documents:
++
[source,js]
-----------------------------------
-POST /logstash-2019.05.18/_doc
+----
+POST /my-index-000001/_doc
{
"@timestamp": "2019-05-18T15:57:27.541Z",
"ip": "225.44.217.191",
@@ -177,7 +242,7 @@ POST /logstash-2019.05.18/_doc
"url": "https://media-for-the-masses.theacademyofperformingartsandscience.org/uploads/charles-fullerton.jpg"
}
-POST /logstash-2019.05.20/_doc
+POST /my-index-000002/_doc
{
"@timestamp": "2019-05-20T03:44:20.844Z",
"ip": "198.247.165.49",
@@ -192,7 +257,10 @@ POST /logstash-2019.05.20/_doc
"memory": 241720,
"url": "https://theacademyofperformingartsandscience.org/people/type:astronauts/name:laurel-b-clark/profile"
}
-----------------------------------
+----
++
+These requests create two indices: `my-index-000001` and `my-index-000002`.
-The mappings and alias are configured automatically based on the template. To verify, you
-can view one of the newly created indices using the {ref}/indices-get-index.html#indices-get-index[index API].
+. Use the {es} {ref}/indices-get-index.html#indices-get-index[get index API] to
+view one of the newly created indices. The index's mappings and alias are
+configured automatically based on the template.
diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc
index d8c200450d7e52..7fc8fe5114e1e4 100644
--- a/docs/redirects.asciidoc
+++ b/docs/redirects.asciidoc
@@ -28,12 +28,12 @@ This page has moved. Please see the new section in the {heartbeat-ref}/securing-
[role="exclude",id="infra-read-only-access"]
== Configure source read-only access
-This page has moved. Please see the new section in the {metrics-guide}/configure-metrics-source.html[Metrics Monitoring Guide].
+This page has moved. Please see {observability-guide}/configure-settings.html[configure settings].
[role="exclude",id="logs-read-only-access"]
== Configure source read-only access
-This page has moved. Please see {logs-guide}/configure-logs-source.html[logs configuration].
+This page has moved. Please see {observability-guide}/configure-data-sources.html[configure data sources].
[role="exclude",id="extend"]
== Extend your use case
diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc
index 917821ad09e2fc..f48dbeab9d61a8 100644
--- a/docs/settings/monitoring-settings.asciidoc
+++ b/docs/settings/monitoring-settings.asciidoc
@@ -76,6 +76,9 @@ a|`monitoring.cluster_alerts.`
health checks. By default, it matches the <> setting,
which has a default value of `30000`.
+| `monitoring.ui.elasticsearch.ssl`
+ | Shares the same configuration as <>. These settings configure encrypted communication between {kib} and the monitoring cluster.
+
|===
[float]
diff --git a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts
index 340f45a0a2c189..b7ffefe7005e1d 100644
--- a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts
+++ b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts
@@ -24,7 +24,8 @@ const { startES } = kbnTestServer.createTestServers({
});
let esServer: kbnTestServer.TestElasticsearchUtils;
-describe('default route provider', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/81072
+describe.skip('default route provider', () => {
let root: Root;
beforeAll(async () => {
diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap
index ed84aceb60e5ab..dc4da2456b47be 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap
+++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap
@@ -2,6 +2,7 @@
exports[`IndexPattern toSpec should match snapshot 1`] = `
Object {
+ "fieldFormats": Object {},
"fields": Object {
"@tags": Object {
"aggregatable": true,
diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap
index 752fdcf11991ce..a3d19f311b7654 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap
+++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap
@@ -2,6 +2,9 @@
exports[`IndexPatterns savedObjectToSpec 1`] = `
Object {
+ "fieldFormats": Object {
+ "field": Object {},
+ },
"fields": Object {},
"id": "id",
"intervalName": undefined,
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
index 6e11bc8f1d508f..9fd43be8dc5b37 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
@@ -183,6 +183,20 @@ describe('IndexPattern', () => {
});
});
+ describe('setFieldFormat and deleteFieldFormaat', () => {
+ test('should persist changes', () => {
+ const formatter = {
+ toJSON: () => ({ id: 'bytes' }),
+ } as FieldFormat;
+ indexPattern.getFormatterForField = () => formatter;
+ indexPattern.setFieldFormat('bytes', { id: 'bytes' });
+ expect(indexPattern.toSpec().fieldFormats).toEqual({ bytes: { id: 'bytes' } });
+
+ indexPattern.deleteFieldFormat('bytes');
+ expect(indexPattern.toSpec().fieldFormats).toEqual({});
+ });
+ });
+
describe('toSpec', () => {
test('should match snapshot', () => {
const formatter = {
@@ -209,7 +223,6 @@ describe('IndexPattern', () => {
expect(restoredPattern.title).toEqual(indexPattern.title);
expect(restoredPattern.timeFieldName).toEqual(indexPattern.timeFieldName);
expect(restoredPattern.fields.length).toEqual(indexPattern.fields.length);
- expect(restoredPattern.fieldFormatMap.bytes instanceof MockFieldFormatter).toEqual(true);
});
});
});
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
index 5fc6344c935d5b..d38df68e9f4286 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
@@ -21,13 +21,7 @@ import _, { each, reject } from 'lodash';
import { SavedObjectsClientCommon } from '../..';
import { DuplicateField } from '../../../../kibana_utils/common';
-import {
- ES_FIELD_TYPES,
- KBN_FIELD_TYPES,
- IIndexPattern,
- FieldFormatNotFoundError,
- IFieldType,
-} from '../../../common';
+import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common';
import { IndexPatternField, IIndexPatternFieldList, fieldList } from '../fields';
import { formatHitProvider } from './format_hit';
import { flattenHitWrapper } from './flatten_hit';
@@ -102,7 +96,7 @@ export class IndexPattern implements IIndexPattern {
// set values
this.id = spec.id;
- const fieldFormatMap = this.fieldSpecsToFieldFormatMap(spec.fields);
+ this.fieldFormatMap = spec.fieldFormats || {};
this.version = spec.version;
@@ -113,12 +107,16 @@ export class IndexPattern implements IIndexPattern {
this.fields.replaceAll(Object.values(spec.fields || {}));
this.type = spec.type;
this.typeMeta = spec.typeMeta;
-
- this.fieldFormatMap = _.mapValues(fieldFormatMap, (mapping) => {
- return this.deserializeFieldFormatMap(mapping);
- });
}
+ setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => {
+ this.fieldFormatMap[fieldName] = format;
+ };
+
+ deleteFieldFormat = (fieldName: string) => {
+ delete this.fieldFormatMap[fieldName];
+ };
+
/**
* Get last saved saved object fields
*/
@@ -131,34 +129,6 @@ export class IndexPattern implements IIndexPattern {
this.originalSavedObjectBody = this.getAsSavedObjectBody();
};
- /**
- * Converts field format spec to field format instance
- * @param mapping
- */
- private deserializeFieldFormatMap(mapping: SerializedFieldFormat>) {
- try {
- return this.fieldFormats.getInstance(mapping.id as string, mapping.params);
- } catch (err) {
- if (err instanceof FieldFormatNotFoundError) {
- return undefined;
- } else {
- throw err;
- }
- }
- }
-
- /**
- * Extracts FieldFormatMap from FieldSpec map
- * @param fldList FieldSpec map
- */
- private fieldSpecsToFieldFormatMap = (fldList: IndexPatternSpec['fields'] = {}) =>
- Object.values(fldList).reduce>((col, fieldSpec) => {
- if (fieldSpec.format) {
- col[fieldSpec.name] = { ...fieldSpec.format };
- }
- return col;
- }, {});
-
getComputedFields() {
const scriptFields: any = {};
if (!this.fields) {
@@ -211,6 +181,7 @@ export class IndexPattern implements IIndexPattern {
fields: this.fields.toSpec({ getFormatterForField: this.getFormatterForField.bind(this) }),
typeMeta: this.typeMeta,
type: this.type,
+ fieldFormats: this.fieldFormatMap,
};
}
@@ -299,17 +270,9 @@ export class IndexPattern implements IIndexPattern {
* Returns index pattern as saved object body for saving
*/
getAsSavedObjectBody() {
- const serializeFieldFormatMap = (
- flat: any,
- format: FieldFormat | undefined,
- field: string | undefined
- ) => {
- if (format && field) {
- flat[field] = format;
- }
- };
- const serialized = _.transform(this.fieldFormatMap, serializeFieldFormatMap);
- const fieldFormatMap = _.isEmpty(serialized) ? undefined : JSON.stringify(serialized);
+ const fieldFormatMap = _.isEmpty(this.fieldFormatMap)
+ ? undefined
+ : JSON.stringify(this.fieldFormatMap);
return {
title: this.title,
@@ -330,12 +293,25 @@ export class IndexPattern implements IIndexPattern {
getFormatterForField(
field: IndexPatternField | IndexPatternField['spec'] | IFieldType
): FieldFormat {
- return (
- this.fieldFormatMap[field.name] ||
- this.fieldFormats.getDefaultInstance(
+ const formatSpec = this.fieldFormatMap[field.name];
+ if (formatSpec) {
+ return this.fieldFormats.getInstance(formatSpec.id, formatSpec.params);
+ } else {
+ return this.fieldFormats.getDefaultInstance(
field.type as KBN_FIELD_TYPES,
field.esTypes as ES_FIELD_TYPES[]
- )
- );
+ );
+ }
+ }
+
+ /**
+ * Get formatter for a given field name. Return undefined if none exists
+ * @param field
+ */
+ getFormatterForFieldNoDefault(fieldname: string) {
+ const formatSpec = this.fieldFormatMap[fieldname];
+ if (formatSpec) {
+ return this.fieldFormats.getInstance(formatSpec.id, formatSpec.params);
+ }
}
}
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
index 9a86541376cd80..bfd0dc9d946c24 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
@@ -35,7 +35,6 @@ import {
IndexPatternSpec,
IndexPatternAttributes,
FieldSpec,
- FieldFormatMap,
IndexPatternFieldMap,
} from '../types';
import { FieldFormatsStartCommon } from '../../field_formats';
@@ -296,20 +295,6 @@ export class IndexPatternsService {
return fields;
};
- /**
- * Applies a set of formats to a set of fields
- * @param fieldSpecs
- * @param fieldFormatMap
- */
- private addFormatsToFields = (fieldSpecs: FieldSpec[], fieldFormatMap: FieldFormatMap) => {
- Object.entries(fieldFormatMap).forEach(([fieldName, value]) => {
- const field = fieldSpecs.find((fld: FieldSpec) => fld.name === fieldName);
- if (field) {
- field.format = value;
- }
- });
- };
-
/**
* Converts field array to map
* @param fields
@@ -346,7 +331,6 @@ export class IndexPatternsService {
const parsedFieldFormatMap = fieldFormatMap ? JSON.parse(fieldFormatMap) : {};
const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : [];
- this.addFormatsToFields(parsedFields, parsedFieldFormatMap);
return {
id,
version,
@@ -357,6 +341,7 @@ export class IndexPatternsService {
fields: this.fieldArrayToMap(parsedFields),
typeMeta: parsedTypeMeta,
type,
+ fieldFormats: parsedFieldFormatMap,
};
};
@@ -382,9 +367,6 @@ export class IndexPatternsService {
const spec = this.savedObjectToSpec(savedObject);
const { title, type, typeMeta } = spec;
- const parsedFieldFormats: FieldFormatMap = savedObject.attributes.fieldFormatMap
- ? JSON.parse(savedObject.attributes.fieldFormatMap)
- : {};
const isFieldRefreshRequired = this.isFieldRefreshRequired(spec.fields);
let isSaveRequired = isFieldRefreshRequired;
@@ -415,12 +397,9 @@ export class IndexPatternsService {
}
}
- Object.entries(parsedFieldFormats).forEach(([fieldName, value]) => {
- const field = spec.fields?.[fieldName];
- if (field) {
- field.format = value;
- }
- });
+ spec.fieldFormats = savedObject.attributes.fieldFormatMap
+ ? JSON.parse(savedObject.attributes.fieldFormatMap)
+ : {};
const indexPattern = await this.create(spec, true);
indexPatternCache.set(id, indexPattern);
diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts
index cb0c3aa0de38ef..3387bc3b3c19e4 100644
--- a/src/plugins/data/common/index_patterns/types.ts
+++ b/src/plugins/data/common/index_patterns/types.ts
@@ -172,6 +172,7 @@ export interface IndexPatternSpec {
fields?: IndexPatternFieldMap;
typeMeta?: TypeMeta;
type?: string;
+ fieldFormats?: Record;
}
export interface SourceFilter {
diff --git a/src/plugins/data/common/kbn_field_types/kbn_field_types.test.ts b/src/plugins/data/common/kbn_field_types/kbn_field_types.test.ts
index 6a2d6edd046923..dd1a9a7f689a93 100644
--- a/src/plugins/data/common/kbn_field_types/kbn_field_types.test.ts
+++ b/src/plugins/data/common/kbn_field_types/kbn_field_types.test.ts
@@ -66,6 +66,7 @@ describe('utils/kbn_field_types', () => {
test('returns the kbnFieldType name that matches the esType', () => {
expect(castEsToKbnFieldTypeName(ES_FIELD_TYPES.KEYWORD)).toBe('string');
expect(castEsToKbnFieldTypeName(ES_FIELD_TYPES.FLOAT)).toBe('number');
+ expect(castEsToKbnFieldTypeName(ES_FIELD_TYPES.UNSIGNED_LONG)).toBe('number');
});
test('returns unknown for unknown es types', () => {
diff --git a/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts b/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts
index b93ebcbbca9c8a..373cdfda306076 100644
--- a/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts
+++ b/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts
@@ -48,6 +48,7 @@ export const createKbnFieldTypes = (): KbnFieldType[] => [
ES_FIELD_TYPES.DOUBLE,
ES_FIELD_TYPES.INTEGER,
ES_FIELD_TYPES.LONG,
+ ES_FIELD_TYPES.UNSIGNED_LONG,
ES_FIELD_TYPES.SHORT,
ES_FIELD_TYPES.BYTE,
ES_FIELD_TYPES.TOKEN_COUNT,
diff --git a/src/plugins/data/common/kbn_field_types/types.ts b/src/plugins/data/common/kbn_field_types/types.ts
index acd7a36b01fb30..ba9fd3e70b3152 100644
--- a/src/plugins/data/common/kbn_field_types/types.ts
+++ b/src/plugins/data/common/kbn_field_types/types.ts
@@ -52,6 +52,7 @@ export enum ES_FIELD_TYPES {
INTEGER = 'integer',
LONG = 'long',
SHORT = 'short',
+ UNSIGNED_LONG = 'unsigned_long',
NESTED = 'nested',
BYTE = 'byte',
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index d280b6f1faf7d4..1390b28ec830db 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -514,7 +514,9 @@ export enum ES_FIELD_TYPES {
// (undocumented)
TOKEN_COUNT = "token_count",
// (undocumented)
- _TYPE = "_type"
+ _TYPE = "_type",
+ // (undocumented)
+ UNSIGNED_LONG = "unsigned_long"
}
// Warning: (ae-missing-release-tag) "ES_SEARCH_STRATEGY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -1063,6 +1065,8 @@ export class IndexPattern implements IIndexPattern {
constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps);
addScriptedField(name: string, script: string, fieldType?: string): Promise;
// (undocumented)
+ deleteFieldFormat: (fieldName: string) => void;
+ // (undocumented)
fieldFormatMap: Record;
// (undocumented)
fields: IIndexPatternFieldList & {
@@ -1108,6 +1112,7 @@ export class IndexPattern implements IIndexPattern {
// (undocumented)
getFieldByName(name: string): IndexPatternField | undefined;
getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat;
+ getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined;
// (undocumented)
getNonScriptedFields(): IndexPatternField[];
getOriginalSavedObjectBody: () => {
@@ -1139,6 +1144,8 @@ export class IndexPattern implements IIndexPattern {
metaFields: string[];
removeScriptedField(fieldName: string): void;
resetOriginalSavedObjectBody: () => void;
+ // (undocumented)
+ setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void;
// Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@@ -1287,6 +1294,8 @@ export type IndexPatternSelectProps = Required, 'isLo
//
// @public (undocumented)
export interface IndexPatternSpec {
+ // (undocumented)
+ fieldFormats?: Record;
// (undocumented)
fields?: IndexPatternFieldMap;
// (undocumented)
@@ -2274,7 +2283,7 @@ export const UI_SETTINGS: {
// src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:70:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index 0ed296a1d0662d..65313adfc0e0f2 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -211,7 +211,9 @@ export enum ES_FIELD_TYPES {
// (undocumented)
TOKEN_COUNT = "token_count",
// (undocumented)
- _TYPE = "_type"
+ _TYPE = "_type",
+ // (undocumented)
+ UNSIGNED_LONG = "unsigned_long"
}
// Warning: (ae-missing-release-tag) "ES_SEARCH_STRATEGY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -513,6 +515,8 @@ export class IndexPattern implements IIndexPattern {
constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps);
addScriptedField(name: string, script: string, fieldType?: string): Promise;
// (undocumented)
+ deleteFieldFormat: (fieldName: string) => void;
+ // (undocumented)
fieldFormatMap: Record;
// Warning: (ae-forgotten-export) The symbol "IIndexPatternFieldList" needs to be exported by the entry point index.d.ts
//
@@ -560,6 +564,7 @@ export class IndexPattern implements IIndexPattern {
// (undocumented)
getFieldByName(name: string): IndexPatternField | undefined;
getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat;
+ getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined;
// Warning: (ae-forgotten-export) The symbol "IndexPatternField" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@@ -593,6 +598,10 @@ export class IndexPattern implements IIndexPattern {
metaFields: string[];
removeScriptedField(fieldName: string): void;
resetOriginalSavedObjectBody: () => void;
+ // Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
+ setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void;
// Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@@ -1104,8 +1113,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage;
//
// src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:70:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:58:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/discover/public/application/angular/context/query/actions.js b/src/plugins/discover/public/application/angular/context/query/actions.js
index 32fc2873d7f2a2..d5c72d34006e2e 100644
--- a/src/plugins/discover/public/application/angular/context/query/actions.js
+++ b/src/plugins/discover/public/application/angular/context/query/actions.js
@@ -25,7 +25,7 @@ import { getServices } from '../../../../kibana_services';
import { fetchAnchorProvider } from '../api/anchor';
import { fetchContextProvider } from '../api/context';
import { getQueryParameterActions } from '../query_parameters';
-import { FAILURE_REASONS, LOADING_STATUS } from './constants';
+import { FAILURE_REASONS, LOADING_STATUS } from './index';
import { MarkdownSimple } from '../../../../../../kibana_react/public';
export function QueryActionsProvider(Promise) {
diff --git a/src/plugins/discover/public/application/angular/context/query/index.js b/src/plugins/discover/public/application/angular/context/query/index.js
index f9b1a35e32feac..59d1f165d19d4d 100644
--- a/src/plugins/discover/public/application/angular/context/query/index.js
+++ b/src/plugins/discover/public/application/angular/context/query/index.js
@@ -18,5 +18,5 @@
*/
export { QueryActionsProvider } from './actions';
-export { FAILURE_REASONS, LOADING_STATUS } from './constants';
+export { FAILURE_REASONS, LOADING_STATUS } from '../../../components/context_app/constants';
export { createInitialLoadingStatusState } from './state';
diff --git a/src/plugins/discover/public/application/angular/context/query/state.js b/src/plugins/discover/public/application/angular/context/query/state.js
index 06fd0680d347f7..142b5746249bf1 100644
--- a/src/plugins/discover/public/application/angular/context/query/state.js
+++ b/src/plugins/discover/public/application/angular/context/query/state.js
@@ -17,7 +17,7 @@
* under the License.
*/
-import { LOADING_STATUS } from './constants';
+import { LOADING_STATUS } from './index';
export function createInitialLoadingStatusState() {
return {
diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html
index 6adcaeeae94f55..d609a497c4ba1e 100644
--- a/src/plugins/discover/public/application/angular/context_app.html
+++ b/src/plugins/discover/public/application/angular/context_app.html
@@ -12,8 +12,8 @@
-
@@ -35,39 +35,17 @@
type="'predecessors'"
>
-
-
-
-
-
+
,
-
-
-
-
-
-
-
-
- Refine your query
-
-
- The search bar at the top uses Elasticsearch’s support for Lucene
-
- Query String syntax
-
- . Here are some examples of how you can search for web server logs that have been parsed into a few fields.
-
-
-
-
-
-
-
- Find requests that contain the number 200, in any field
-
-
-
-
-
-
- 200
-
-
-
-
-
-
- Find 200 in the status field
-
-
-
-
-
-
- status:200
-
-
-
-
-
-
- Find all status codes between 400-499
-
-
-
-
-
-
- status:[400 TO 499]
-
-
-
-
-
-
- Find status codes 400-499 with the extension php
-
-
-
-
-
-
- status:[400 TO 499] AND extension:PHP
-
-
-
-
-
-
- Find status codes 400-499 with the extension php or html
-
-
-
-
-
-
- status:[400 TO 499] AND (extension:php OR extension:html)
-
-
-
-
-
-
-
,
-]
-`;
-
-exports[`DiscoverNoResults props timeFieldName renders time range feedback 1`] = `
-Array [
-
,
-
-
-
-
-
-
-
-
- Expand your time range
-
-
- One or more of the indices you’re looking at contains a date field. Your query may not match anything in the current time range, or there may not be any data at all in the currently selected time range. You can try changing the time range to one which contains data.
-
-
-
-
,
-]
-`;
diff --git a/src/plugins/discover/public/application/angular/directives/_index.scss b/src/plugins/discover/public/application/angular/directives/_index.scss
index d4b365547b40c5..dfacdf45c9d7bf 100644
--- a/src/plugins/discover/public/application/angular/directives/_index.scss
+++ b/src/plugins/discover/public/application/angular/directives/_index.scss
@@ -1,2 +1 @@
-@import 'no_results';
@import 'histogram';
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts b/src/plugins/discover/public/application/angular/directives/index.ts
similarity index 72%
rename from src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts
rename to src/plugins/discover/public/application/angular/directives/index.ts
index 861017d99962ec..2e120995cce073 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts
+++ b/src/plugins/discover/public/application/angular/directives/index.ts
@@ -17,11 +17,5 @@
* under the License.
*/
-import { get } from 'lodash';
-import { IIndexPattern } from '../../../../../../data/public';
-
-export function getFieldFormat(indexPattern?: IIndexPattern, fieldName?: string): string {
- return indexPattern && fieldName
- ? get(indexPattern, ['fieldFormatMap', fieldName, 'type', 'title'])
- : '';
-}
+export { DiscoverUninitialized } from './uninitialized';
+export { DiscoverHistogram } from './histogram';
diff --git a/src/plugins/discover/public/application/angular/directives/no_results.js b/src/plugins/discover/public/application/angular/directives/no_results.js
deleted file mode 100644
index d8a39d9178e937..00000000000000
--- a/src/plugins/discover/public/application/angular/directives/no_results.js
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import React, { Component, Fragment } from 'react';
-import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
-import PropTypes from 'prop-types';
-
-import {
- EuiCallOut,
- EuiCode,
- EuiDescriptionList,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLink,
- EuiSpacer,
- EuiText,
-} from '@elastic/eui';
-import { getServices } from '../../../kibana_services';
-
-// eslint-disable-next-line react/prefer-stateless-function
-export class DiscoverNoResults extends Component {
- static propTypes = {
- timeFieldName: PropTypes.string,
- queryLanguage: PropTypes.string,
- };
-
- render() {
- const { timeFieldName, queryLanguage } = this.props;
-
- let timeFieldMessage;
-
- if (timeFieldName) {
- timeFieldMessage = (
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- let luceneQueryMessage;
-
- if (queryLanguage === 'lucene') {
- const searchExamples = [
- {
- description: 200 ,
- title: (
-
-
-
-
-
- ),
- },
- {
- description: status:200 ,
- title: (
-
-
-
-
-
- ),
- },
- {
- description: status:[400 TO 499] ,
- title: (
-
-
-
-
-
- ),
- },
- {
- description: status:[400 TO 499] AND extension:PHP ,
- title: (
-
-
-
-
-
- ),
- },
- {
- description: status:[400 TO 499] AND (extension:php OR extension:html) ,
- title: (
-
-
-
-
-
- ),
- },
- ];
-
- luceneQueryMessage = (
-
-
-
-
-
-
-
-
-
-
-
-
- ),
- }}
- />
-
-
-
-
-
-
-
-
-
- );
- }
-
- return (
-
-
-
-
-
-
-
- }
- color="warning"
- iconType="help"
- data-test-subj="discoverNoResults"
- />
- {timeFieldMessage}
- {luceneQueryMessage}
-
-
-
-
- );
- }
-}
diff --git a/src/plugins/discover/public/application/angular/directives/no_results.test.js b/src/plugins/discover/public/application/angular/directives/no_results.test.js
deleted file mode 100644
index 60c50048a39ef5..00000000000000
--- a/src/plugins/discover/public/application/angular/directives/no_results.test.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import React from 'react';
-import { renderWithIntl } from 'test_utils/enzyme_helpers';
-
-import { DiscoverNoResults } from './no_results';
-
-jest.mock('../../../kibana_services', () => {
- return {
- getServices: () => ({
- docLinks: {
- links: {
- query: {
- luceneQuerySyntax: 'documentation-link',
- },
- },
- },
- }),
- };
-});
-
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
-describe('DiscoverNoResults', () => {
- describe('props', () => {
- describe('timeFieldName', () => {
- test('renders time range feedback', () => {
- const component = renderWithIntl( );
-
- expect(component).toMatchSnapshot();
- });
- });
-
- describe('queryLanguage', () => {
- test('supports lucene and renders doc link', () => {
- const component = renderWithIntl(
- 'documentation-link'} />
- );
-
- expect(component).toMatchSnapshot();
- });
- });
- });
-});
diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js
index 612cedb7780bd3..ebd086dd1e38a6 100644
--- a/src/plugins/discover/public/application/angular/discover.js
+++ b/src/plugins/discover/public/application/angular/discover.js
@@ -87,6 +87,7 @@ const fetchStatuses = {
UNINITIALIZED: 'uninitialized',
LOADING: 'loading',
COMPLETE: 'complete',
+ ERROR: 'error',
};
const app = getAngularModule();
@@ -620,6 +621,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
config: config,
fixedScroll: createFixedScroll($scope, $timeout),
setHeaderActionMenu: getHeaderActionMenuMounter(),
+ data,
};
const shouldSearchOnPageLoad = () => {
@@ -685,7 +687,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
function pick(rows, oldRows, fetchStatus) {
// initial state, pretend we're already loading if we're about to execute a search so
// that the uninitilized message doesn't flash on screen
- if (rows == null && oldRows == null && shouldSearchOnPageLoad()) {
+ if (!$scope.fetchError && rows == null && oldRows == null && shouldSearchOnPageLoad()) {
return status.LOADING;
}
@@ -814,7 +816,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
if (error instanceof Error && error.name === 'AbortError') return;
$scope.fetchStatus = fetchStatuses.NO_RESULTS;
- $scope.rows = [];
+ $scope.fetchError = error;
data.search.showError(error);
});
diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx
index ad2b674af014c8..f191fa2dc89e84 100644
--- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx
+++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx
@@ -68,6 +68,7 @@ export function convertDirectiveToRenderFn(
let rejected = false;
const cleanupFnPromise = injectAngularElement(domNode, directive.template, props, getInjector);
+
cleanupFnPromise.catch(() => {
rejected = true;
render(error
, domNode);
@@ -91,10 +92,10 @@ export interface DocTableLegacyProps {
rows: Array>;
indexPattern: IIndexPattern;
minimumVisibleRows: number;
- onAddColumn: (column: string) => void;
- onSort: (sort: string[][]) => void;
- onMoveColumn: (columns: string, newIdx: number) => void;
- onRemoveColumn: (column: string) => void;
+ onAddColumn?: (column: string) => void;
+ onSort?: (sort: string[][]) => void;
+ onMoveColumn?: (columns: string, newIdx: number) => void;
+ onRemoveColumn?: (column: string) => void;
sort?: string[][];
}
diff --git a/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap b/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap
new file mode 100644
index 00000000000000..58305ee23cb210
--- /dev/null
+++ b/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap
@@ -0,0 +1,741 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ContextAppLegacy test renders correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`ContextAppLegacy test renders loading indicator 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ Loading...
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/plugins/discover/public/application/angular/context/query/constants.js b/src/plugins/discover/public/application/components/context_app/constants.ts
similarity index 100%
rename from src/plugins/discover/public/application/angular/context/query/constants.js
rename to src/plugins/discover/public/application/components/context_app/constants.ts
diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx
new file mode 100644
index 00000000000000..16d8cd78004f92
--- /dev/null
+++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx
@@ -0,0 +1,75 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { ContextAppLegacy } from './context_app_legacy';
+import { IIndexPattern } from '../../../../../data/common/index_patterns';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react';
+import { findTestSubject } from '@elastic/eui/lib/test';
+
+describe('ContextAppLegacy test', () => {
+ const hit = {
+ _id: '123',
+ _index: 'test_index',
+ _score: null,
+ _version: 1,
+ _source: {
+ category: ["Men's Clothing"],
+ currency: 'EUR',
+ customer_first_name: 'Walker',
+ customer_full_name: 'Walker Texas Ranger',
+ customer_gender: 'MALE',
+ customer_last_name: 'Ranger',
+ },
+ fields: [{ order_date: ['2020-10-19T13:35:02.000Z'] }],
+ sort: [1603114502000, 2092],
+ };
+ const indexPattern = {
+ id: 'test_index_pattern',
+ } as IIndexPattern;
+ const defaultProps = {
+ columns: ['_source'],
+ filter: () => {},
+ hits: [hit],
+ infiniteScroll: true,
+ sorting: ['order_date', 'desc'],
+ minimumVisibleRows: 5,
+ indexPattern,
+ status: 'loaded',
+ };
+
+ it('renders correctly', () => {
+ const component = mountWithIntl( );
+ expect(component).toMatchSnapshot();
+ expect(component.find(DocTableLegacy).length).toBe(1);
+ const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator');
+ expect(loadingIndicator.length).toBe(0);
+ });
+
+ it('renders loading indicator', () => {
+ const props = { ...defaultProps };
+ props.status = 'loading';
+ const component = mountWithIntl( );
+ expect(component).toMatchSnapshot();
+ expect(component.find('DocTableLegacy').length).toBe(0);
+ const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator');
+ expect(loadingIndicator.length).toBe(1);
+ });
+});
diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx
new file mode 100644
index 00000000000000..ee8b2f590f71c0
--- /dev/null
+++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx
@@ -0,0 +1,76 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiPanel, EuiText } from '@elastic/eui';
+import { I18nProvider } from '@kbn/i18n/react';
+import {
+ DocTableLegacy,
+ DocTableLegacyProps,
+} from '../../angular/doc_table/create_doc_table_react';
+import { IIndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns';
+import { LOADING_STATUS } from './constants';
+
+export interface ContextAppProps {
+ columns: string[];
+ hits: Array>;
+ indexPattern: IIndexPattern;
+ filter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
+ minimumVisibleRows: number;
+ sorting: string[];
+ status: string;
+}
+
+export function ContextAppLegacy(renderProps: ContextAppProps) {
+ const { hits, filter, sorting, status } = renderProps;
+ const props = ({ ...renderProps } as unknown) as DocTableLegacyProps;
+ props.rows = hits;
+ props.onFilter = filter;
+ props.sort = sorting.map((el) => [el]);
+ const isLoaded = status === LOADING_STATUS.LOADED;
+ const loadingFeedback = () => {
+ if (status === LOADING_STATUS.UNINITIALIZED || status === LOADING_STATUS.LOADING) {
+ return (
+
+
+
+
+
+ );
+ }
+ return null;
+ };
+ return (
+
+
+ {loadingFeedback()}
+ {isLoaded ? (
+
+
+
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/src/plugins/discover/public/application/angular/directives/index.js b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts
similarity index 58%
rename from src/plugins/discover/public/application/angular/directives/index.js
rename to src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts
index 5d8969a78f0182..af94c5537da288 100644
--- a/src/plugins/discover/public/application/angular/directives/index.js
+++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts
@@ -16,16 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { ContextAppLegacy } from './context_app_legacy';
-import { DiscoverNoResults } from './no_results';
-import { DiscoverUninitialized } from './uninitialized';
-import { DiscoverHistogram } from './histogram';
-import { getAngularModule } from '../../../kibana_services';
-
-const app = getAngularModule();
-
-app.directive('discoverNoResults', (reactDirective) => reactDirective(DiscoverNoResults));
-
-app.directive('discoverUninitialized', (reactDirective) => reactDirective(DiscoverUninitialized));
-
-app.directive('discoverHistogram', (reactDirective) => reactDirective(DiscoverHistogram));
+export function createContextAppLegacy(reactDirective: any) {
+ return reactDirective(ContextAppLegacy, [
+ ['filter', { watchDepth: 'reference' }],
+ ['hits', { watchDepth: 'reference' }],
+ ['indexPattern', { watchDepth: 'reference' }],
+ ['sorting', { watchDepth: 'reference' }],
+ ['columns', { watchDepth: 'collection' }],
+ ['infiniteScroll', { watchDepth: 'reference' }],
+ ['minimumVisibleRows', { watchDepth: 'reference' }],
+ ['status', { watchDepth: 'reference' }],
+ ]);
+}
diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx
index 139b2ca69d9e47..3ca421f809640b 100644
--- a/src/plugins/discover/public/application/components/discover_legacy.tsx
+++ b/src/plugins/discover/public/application/components/discover_legacy.tsx
@@ -26,10 +26,8 @@ import { HitsCounter } from './hits_counter';
import { TimechartHeader } from './timechart_header';
import { DiscoverSidebar } from './sidebar';
import { getServices, IndexPattern } from '../../kibana_services';
-// @ts-ignore
-import { DiscoverNoResults } from '../angular/directives/no_results';
-import { DiscoverUninitialized } from '../angular/directives/uninitialized';
-import { DiscoverHistogram } from '../angular/directives/histogram';
+import { DiscoverUninitialized, DiscoverHistogram } from '../angular/directives';
+import { DiscoverNoResults } from './no_results';
import { LoadingSpinner } from './loading_spinner/loading_spinner';
import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react';
import { SkipBottomButton } from './skip_bottom_button';
@@ -40,6 +38,7 @@ import {
TimeRange,
Query,
IndexPatternAttributes,
+ DataPublicPluginStart,
} from '../../../../data/public';
import { Chart } from '../angular/helpers/point_series';
import { AppState } from '../angular/discover_state';
@@ -53,6 +52,7 @@ export interface DiscoverLegacyProps {
addColumn: (column: string) => void;
fetch: () => void;
fetchCounter: number;
+ fetchError: Error;
fieldCounts: Record;
histogramData: Chart;
hits: number;
@@ -73,6 +73,7 @@ export interface DiscoverLegacyProps {
sampleSize: number;
fixedScroll: (el: HTMLElement) => void;
setHeaderActionMenu: (menuMount: MountPoint | undefined) => void;
+ data: DataPublicPluginStart;
};
resetQuery: () => void;
resultState: string;
@@ -94,6 +95,7 @@ export function DiscoverLegacy({
fetch,
fetchCounter,
fieldCounts,
+ fetchError,
histogramData,
hits,
indexPattern,
@@ -208,6 +210,8 @@ export function DiscoverLegacy({
)}
{resultState === 'uninitialized' && }
diff --git a/src/plugins/discover/public/application/angular/directives/_no_results.scss b/src/plugins/discover/public/application/components/no_results/_no_results.scss
similarity index 100%
rename from src/plugins/discover/public/application/angular/directives/_no_results.scss
rename to src/plugins/discover/public/application/components/no_results/_no_results.scss
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/index.ts b/src/plugins/discover/public/application/components/no_results/index.ts
similarity index 93%
rename from src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/index.ts
rename to src/plugins/discover/public/application/components/no_results/index.ts
index 9ab950fbfb2f20..afe35b1fd7c18f 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/index.ts
+++ b/src/plugins/discover/public/application/components/no_results/index.ts
@@ -17,4 +17,4 @@
* under the License.
*/
-export { getFieldFormat } from './get_field_format';
+export { DiscoverNoResults } from './no_results';
diff --git a/src/plugins/discover/public/application/components/no_results/no_results.test.tsx b/src/plugins/discover/public/application/components/no_results/no_results.test.tsx
new file mode 100644
index 00000000000000..dde75236eb15ec
--- /dev/null
+++ b/src/plugins/discover/public/application/components/no_results/no_results.test.tsx
@@ -0,0 +1,118 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { findTestSubject } from '@elastic/eui/lib/test';
+
+import { DiscoverNoResults, DiscoverNoResultsProps } from './no_results';
+
+jest.mock('../../../kibana_services', () => {
+ return {
+ getServices: () => ({
+ docLinks: {
+ links: {
+ query: {
+ luceneQuerySyntax: 'documentation-link',
+ },
+ },
+ },
+ }),
+ };
+});
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+function mountAndFindSubjects(props: DiscoverNoResultsProps) {
+ const component = mountWithIntl( );
+ return {
+ mainMsg: findTestSubject(component, 'discoverNoResults').length > 0,
+ timeFieldMsg: findTestSubject(component, 'discoverNoResultsTimefilter').length > 0,
+ luceneMsg: findTestSubject(component, 'discoverNoResultsLucene').length > 0,
+ errorMsg: findTestSubject(component, 'discoverNoResultsError').length > 0,
+ };
+}
+
+describe('DiscoverNoResults', () => {
+ describe('props', () => {
+ describe('no props', () => {
+ test('renders default feedback', () => {
+ const result = mountAndFindSubjects({});
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "errorMsg": false,
+ "luceneMsg": false,
+ "mainMsg": true,
+ "timeFieldMsg": false,
+ }
+ `);
+ });
+ });
+ describe('timeFieldName', () => {
+ test('renders time range feedback', () => {
+ const result = mountAndFindSubjects({
+ timeFieldName: 'awesome_time_field',
+ });
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "errorMsg": false,
+ "luceneMsg": false,
+ "mainMsg": true,
+ "timeFieldMsg": true,
+ }
+ `);
+ });
+ });
+
+ describe('queryLanguage', () => {
+ test('supports lucene and renders doc link', () => {
+ const result = mountAndFindSubjects({ queryLanguage: 'lucene' });
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "errorMsg": false,
+ "luceneMsg": true,
+ "mainMsg": true,
+ "timeFieldMsg": false,
+ }
+ `);
+ });
+ });
+
+ describe('error message', () => {
+ test('renders error message', () => {
+ const error = new Error('Fatal error');
+ const result = mountAndFindSubjects({
+ timeFieldName: 'awesome_time_field',
+ error,
+ queryLanguage: 'lucene',
+ });
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "errorMsg": true,
+ "luceneMsg": false,
+ "mainMsg": false,
+ "timeFieldMsg": false,
+ }
+ `);
+ });
+ });
+ });
+});
diff --git a/src/plugins/discover/public/application/components/no_results/no_results.tsx b/src/plugins/discover/public/application/components/no_results/no_results.tsx
new file mode 100644
index 00000000000000..fcc2912d16dd5d
--- /dev/null
+++ b/src/plugins/discover/public/application/components/no_results/no_results.tsx
@@ -0,0 +1,92 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { Fragment } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { getServices } from '../../../kibana_services';
+import { DataPublicPluginStart } from '../../../../../data/public';
+import { getLuceneQueryMessage, getTimeFieldMessage } from './no_results_helper';
+import './_no_results.scss';
+
+export interface DiscoverNoResultsProps {
+ timeFieldName?: string;
+ queryLanguage?: string;
+ error?: Error;
+ data?: DataPublicPluginStart;
+}
+
+export function DiscoverNoResults({
+ timeFieldName,
+ queryLanguage,
+ error,
+ data,
+}: DiscoverNoResultsProps) {
+ const callOut = !error ? (
+
+
+ }
+ color="warning"
+ iconType="help"
+ data-test-subj="discoverNoResults"
+ />
+ {timeFieldName ? getTimeFieldMessage() : null}
+ {queryLanguage === 'lucene'
+ ? getLuceneQueryMessage(getServices().docLinks.links.query.luceneQuerySyntax)
+ : null}
+
+ ) : (
+
+
+ }
+ color="danger"
+ iconType="alert"
+ data-test-subj="discoverNoResultsError"
+ >
+ (data ? data.search.showError(error) : void 0)}
+ >
+
+
+
+
+ );
+
+ return (
+
+
+ {callOut}
+
+ );
+}
diff --git a/src/plugins/discover/public/application/components/no_results/no_results_helper.tsx b/src/plugins/discover/public/application/components/no_results/no_results_helper.tsx
new file mode 100644
index 00000000000000..fbc655e01bdf52
--- /dev/null
+++ b/src/plugins/discover/public/application/components/no_results/no_results_helper.tsx
@@ -0,0 +1,149 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { Fragment } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiCode, EuiDescriptionList, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
+
+export function getTimeFieldMessage() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function getLuceneQueryMessage(link: string) {
+ const searchExamples = [
+ {
+ description: 200 ,
+ title: (
+
+
+
+
+
+ ),
+ },
+ {
+ description: status:200 ,
+ title: (
+
+
+
+
+
+ ),
+ },
+ {
+ description: status:[400 TO 499] ,
+ title: (
+
+
+
+
+
+ ),
+ },
+ {
+ description: status:[400 TO 499] AND extension:PHP ,
+ title: (
+
+
+
+
+
+ ),
+ },
+ {
+ description: status:[400 TO 499] AND (extension:php OR extension:html) ,
+ title: (
+
+
+
+
+
+ ),
+ },
+ ];
+ return (
+
+
+
+
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts
index 1ca0bb20e8723b..55a75240909bf5 100644
--- a/src/plugins/discover/public/get_inner_angular.ts
+++ b/src/plugins/discover/public/get_inner_angular.ts
@@ -36,6 +36,7 @@ import {
createToolBarPagerButtonsDirective,
createToolBarPagerTextDirective,
} from './application/angular/doc_table/components/pager';
+import { createContextAppLegacy } from './application/components/context_app/context_app_legacy_directive';
import { createTableRowDirective } from './application/angular/doc_table/components/table_row';
import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory';
import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll';
@@ -55,7 +56,6 @@ import { createContextErrorMessageDirective } from './application/components/con
import { DiscoverStartPlugins } from './plugin';
import { getScopedHistory } from './kibana_services';
import { createDiscoverLegacyDirective } from './application/components/create_discover_legacy_directive';
-
/**
* returns the main inner angular module, it contains all the parts of Angular Discover
* needs to render, so in the end the current 'kibana' angular module is no longer necessary
@@ -190,5 +190,6 @@ function createDocTableModule() {
.directive('kbnTableRow', createTableRowDirective)
.directive('toolBarPagerButtons', createToolBarPagerButtonsDirective)
.directive('kbnInfiniteScroll', createInfiniteScrollDirective)
- .directive('docViewer', createDocViewerDirective);
+ .directive('docViewer', createDocViewerDirective)
+ .directive('contextAppLegacy', createContextAppLegacy);
}
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx
index d9095944eaa335..a086b447994eb9 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx
@@ -133,7 +133,7 @@ describe(' ', () => {
find('btn').simulate('click').update();
});
- expect(onFormData.mock.calls.length).toBe(1);
+ expect(onFormData.mock.calls.length).toBe(2);
const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters<
OnUpdateHandler
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
index ac141baf8fc719..de4edc1edf8735 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
@@ -22,13 +22,16 @@ import React from 'react';
import { FormData } from '../types';
import { useFormData } from '../hooks';
-interface Props {
- children: (formData: FormData) => JSX.Element | null;
+interface Props {
+ children: (formData: I) => JSX.Element | null;
pathsToWatch?: string | string[];
}
-export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => {
- const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch });
+const FormDataProviderComp = function ({
+ children,
+ pathsToWatch,
+}: Props) {
+ const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch });
if (!isReady) {
// No field has mounted yet, don't render anything
@@ -36,4 +39,6 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) =
}
return children(formData);
-});
+};
+
+export const FormDataProvider = React.memo(FormDataProviderComp) as typeof FormDataProviderComp;
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts
index 3c4f9799bb1bce..812a18680d6b8e 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts
@@ -107,7 +107,7 @@ export const UseArray = ({
getNewItemAtIndex,
]);
- // Create a new hook field with the "hasValue" set to false so we don't use its value to build the final form data.
+ // Create a new hook field with the "isIncludedInOutput" set to false so we don't use its value to build the final form data.
// Apart from that the field behaves like a normal field and is hooked into the form validation lifecycle.
const fieldConfigBase: FieldConfig & InternalFieldConfig = {
defaultValue: fieldDefaultValue,
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx
index dbf53a9f0a359a..1a7f8832e4a4e5 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx
@@ -58,7 +58,7 @@ describe(' ', () => {
OnUpdateHandler
>;
- expect(data.raw).toEqual({
+ expect(data.internal).toEqual({
name: 'John',
lastName: 'Snow',
});
@@ -214,8 +214,8 @@ describe(' ', () => {
expect(serializer).not.toBeCalled();
expect(formatter).not.toBeCalled();
- let formData = formHook.getFormData({ unflatten: false });
- expect(formData.name).toEqual('John-deserialized');
+ const internalFormData = formHook.__getFormData$().value;
+ expect(internalFormData.name).toEqual('John-deserialized');
await act(async () => {
form.setInputValue('myField', 'Mike');
@@ -224,9 +224,9 @@ describe(' ', () => {
expect(formatter).toBeCalled(); // Formatters are executed on each value change
expect(serializer).not.toBeCalled(); // Serializer are executed *only** when outputting the form data
- formData = formHook.getFormData();
+ const outputtedFormData = formHook.getFormData();
expect(serializer).toBeCalled();
- expect(formData.name).toEqual('MIKE-serialized');
+ expect(outputtedFormData.name).toEqual('MIKE-serialized');
// Make sure that when we reset the form values, we don't serialize the fields
serializer.mockReset();
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx
index 0670220ccd0c93..6aef6d2b0d46aa 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx
@@ -22,9 +22,9 @@ import React, { createContext, useContext, useMemo } from 'react';
import { FormData, FormHook } from './types';
import { Subject } from './lib';
-export interface Context {
- getFormData$: () => Subject;
- getFormData: FormHook['getFormData'];
+export interface Context {
+ getFormData$: () => Subject;
+ getFormData: FormHook['getFormData'];
}
const FormDataContext = createContext | undefined>(undefined);
@@ -45,6 +45,6 @@ export const FormDataContextProvider = ({ children, getFormData$, getFormData }:
return {children} ;
};
-export function useFormDataContext() {
- return useContext | undefined>(FormDataContext);
+export function useFormDataContext() {
+ return useContext | undefined>(FormDataContext);
}
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts
index 7b21b6638aeac7..f4f13a698ee307 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts
@@ -63,6 +63,7 @@ export const useField = (
__removeField,
__updateFormDataAt,
__validateFields,
+ __getFormData$,
} = form;
const deserializeValue = useCallback(
@@ -76,7 +77,7 @@ export const useField = (
);
const [value, setStateValue] = useState(deserializeValue);
- const [errors, setErrors] = useState([]);
+ const [errors, setStateErrors] = useState([]);
const [isPristine, setPristine] = useState(true);
const [isValidating, setValidating] = useState(false);
const [isChangingValue, setIsChangingValue] = useState(false);
@@ -86,18 +87,12 @@ export const useField = (
const validateCounter = useRef(0);
const changeCounter = useRef(0);
const hasBeenReset = useRef(false);
- const inflightValidation = useRef | null>(null);
+ const inflightValidation = useRef<(Promise & { cancel?(): void }) | null>(null);
const debounceTimeout = useRef(null);
+ // ----------------------------------
// -- HELPERS
// ----------------------------------
- const serializeValue: FieldHook['__serializeValue'] = useCallback(
- (internalValue: I = value) => {
- return serializer ? serializer(internalValue) : ((internalValue as unknown) as T);
- },
- [serializer, value]
- );
-
/**
* Filter an array of errors with specific validation type on them
*
@@ -117,6 +112,11 @@ export const useField = (
);
};
+ /**
+ * If the field has some "formatters" defined in its config, run them in series and return
+ * the transformed value. This handler is called whenever the field value changes, right before
+ * updating the "value" state.
+ */
const formatInputValue = useCallback(
(inputValue: unknown): T => {
const isEmptyString = typeof inputValue === 'string' && inputValue.trim() === '';
@@ -125,11 +125,11 @@ export const useField = (
return inputValue as T;
}
- const formData = getFormData({ unflatten: false });
+ const formData = __getFormData$().value;
return formatters.reduce((output, formatter) => formatter(output, formData), inputValue) as T;
},
- [formatters, getFormData]
+ [formatters, __getFormData$]
);
const onValueChange = useCallback(async () => {
@@ -147,7 +147,7 @@ export const useField = (
// Update the form data observable
__updateFormDataAt(path, value);
- // Validate field(s) (that will update form.isValid state)
+ // Validate field(s) (this will update the form.isValid state)
await __validateFields(fieldsToValidateOnChange ?? [path]);
if (isMounted.current === false) {
@@ -162,15 +162,18 @@ export const useField = (
*/
if (changeIteration === changeCounter.current) {
if (valueChangeDebounceTime > 0) {
- const delta = Date.now() - startTime;
- if (delta < valueChangeDebounceTime) {
+ const timeElapsed = Date.now() - startTime;
+
+ if (timeElapsed < valueChangeDebounceTime) {
+ const timeLeftToWait = valueChangeDebounceTime - timeElapsed;
debounceTimeout.current = setTimeout(() => {
debounceTimeout.current = null;
setIsChangingValue(false);
- }, valueChangeDebounceTime - delta);
+ }, timeLeftToWait);
return;
}
}
+
setIsChangingValue(false);
}
}, [
@@ -183,41 +186,34 @@ export const useField = (
__validateFields,
]);
+ // Cancel any inflight validation (e.g an HTTP Request)
const cancelInflightValidation = useCallback(() => {
- // Cancel any inflight validation (like an HTTP Request)
- if (
- inflightValidation.current &&
- typeof (inflightValidation.current as any).cancel === 'function'
- ) {
- (inflightValidation.current as any).cancel();
+ if (inflightValidation.current && typeof inflightValidation.current.cancel === 'function') {
+ inflightValidation.current.cancel();
inflightValidation.current = null;
}
}, []);
- const clearErrors: FieldHook['clearErrors'] = useCallback(
- (validationType = VALIDATION_TYPES.FIELD) => {
- setErrors((previousErrors) => filterErrors(previousErrors, validationType));
- },
- []
- );
-
const runValidations = useCallback(
- ({
- formData,
- value: valueToValidate,
- validationTypeToValidate,
- }: {
- formData: any;
- value: I;
- validationTypeToValidate?: string;
- }): ValidationError[] | Promise => {
+ (
+ {
+ formData,
+ value: valueToValidate,
+ validationTypeToValidate,
+ }: {
+ formData: any;
+ value: I;
+ validationTypeToValidate?: string;
+ },
+ clearFieldErrors: FieldHook['clearErrors']
+ ): ValidationError[] | Promise => {
if (!validations) {
return [];
}
// By default, for fields that have an asynchronous validation
// we will clear the errors as soon as the field value changes.
- clearErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]);
+ clearFieldErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]);
cancelInflightValidation();
@@ -329,21 +325,33 @@ export const useField = (
// We first try to run the validations synchronously
return runSync();
},
- [clearErrors, cancelInflightValidation, validations, getFormData, getFields, path]
+ [cancelInflightValidation, validations, getFormData, getFields, path]
);
- // -- API
// ----------------------------------
+ // -- Internal API
+ // ----------------------------------
+ const serializeValue: FieldHook['__serializeValue'] = useCallback(
+ (internalValue: I = value) => {
+ return serializer ? serializer(internalValue) : ((internalValue as unknown) as T);
+ },
+ [serializer, value]
+ );
+
+ // ----------------------------------
+ // -- Public API
+ // ----------------------------------
+ const clearErrors: FieldHook['clearErrors'] = useCallback(
+ (validationType = VALIDATION_TYPES.FIELD) => {
+ setStateErrors((previousErrors) => filterErrors(previousErrors, validationType));
+ },
+ []
+ );
- /**
- * Validate a form field, running all its validations.
- * If a validationType is provided then only that validation will be executed,
- * skipping the other type of validation that might exist.
- */
const validate: FieldHook['validate'] = useCallback(
(validationData = {}) => {
const {
- formData = getFormData({ unflatten: false }),
+ formData = __getFormData$().value,
value: valueToValidate = value,
validationType,
} = validationData;
@@ -362,7 +370,7 @@ export const useField = (
// This is the most recent invocation
setValidating(false);
// Update the errors array
- setErrors((prev) => {
+ setStateErrors((prev) => {
const filteredErrors = filterErrors(prev, validationType);
return [...filteredErrors, ..._validationErrors];
});
@@ -374,25 +382,23 @@ export const useField = (
};
};
- const validationErrors = runValidations({
- formData,
- value: valueToValidate,
- validationTypeToValidate: validationType,
- });
+ const validationErrors = runValidations(
+ {
+ formData,
+ value: valueToValidate,
+ validationTypeToValidate: validationType,
+ },
+ clearErrors
+ );
if (Reflect.has(validationErrors, 'then')) {
return (validationErrors as Promise).then(onValidationResult);
}
return onValidationResult(validationErrors as ValidationError[]);
},
- [getFormData, value, runValidations]
+ [__getFormData$, value, runValidations, clearErrors]
);
- /**
- * Handler to change the field value
- *
- * @param newValue The new value to assign to the field
- */
const setValue: FieldHook['setValue'] = useCallback(
(newValue) => {
setStateValue((prev) => {
@@ -408,8 +414,8 @@ export const useField = (
[formatInputValue]
);
- const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => {
- setErrors(
+ const setErrors: FieldHook['setErrors'] = useCallback((_errors) => {
+ setStateErrors(
_errors.map((error) => ({
validationType: VALIDATION_TYPES.FIELD,
__isBlocking__: true,
@@ -418,11 +424,6 @@ export const useField = (
);
}, []);
- /**
- * Form "onChange" event handler
- *
- * @param event Form input change event
- */
const onChange: FieldHook['onChange'] = useCallback(
(event) => {
const newValue = {}.hasOwnProperty.call(event!.target, 'checked')
@@ -485,7 +486,7 @@ export const useField = (
case 'value':
return setValue(nextValue);
case 'errors':
- return setErrors(nextValue);
+ return setStateErrors(nextValue);
case 'isChangingValue':
return setIsChangingValue(nextValue);
case 'isPristine':
@@ -539,7 +540,7 @@ export const useField = (
onChange,
getErrorsMessages,
setValue,
- setErrors: _setErrors,
+ setErrors,
clearErrors,
validate,
reset,
@@ -563,7 +564,7 @@ export const useField = (
onChange,
getErrorsMessages,
setValue,
- _setErrors,
+ setErrors,
clearErrors,
validate,
reset,
@@ -585,7 +586,8 @@ export const useField = (
useEffect(() => {
// If the field value has been reset, we don't want to call the "onValueChange()"
- // as it will set the "isPristine" state to true or validate the field, which initially we don't want.
+ // as it will set the "isPristine" state to true or validate the field, which we don't want
+ // to occur right after resetting the field state.
if (hasBeenReset.current) {
hasBeenReset.current = false;
return;
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx
index b28c09d07fa984..9626aaa9b2459a 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx
@@ -211,7 +211,13 @@ describe('useForm() hook', () => {
test('should allow subscribing to the form data changes and provide a handler to build the form data', async () => {
const TestComp = ({ onData }: { onData: OnUpdateHandler }) => {
- const { form } = useForm();
+ const { form } = useForm({
+ serializer: (value) => ({
+ user: {
+ name: value.user.name.toUpperCase(),
+ },
+ }),
+ });
const { subscribe } = form;
useEffect(() => {
@@ -253,8 +259,9 @@ describe('useForm() hook', () => {
OnUpdateHandler
>;
- expect(data.raw).toEqual({ 'user.name': 'John' });
- expect(data.format()).toEqual({ user: { name: 'John' } });
+ expect(data.internal).toEqual({ user: { name: 'John' } });
+ // Transform name to uppercase as decalred in our serializer func
+ expect(data.format()).toEqual({ user: { name: 'JOHN' } });
// As we have touched all fields, the validity went from "undefined" to "true"
expect(isValid).toBe(true);
});
@@ -302,10 +309,12 @@ describe('useForm() hook', () => {
OnUpdateHandler
>;
- expect(data.raw).toEqual({
+ expect(data.internal).toEqual({
title: defaultValue.title,
subTitle: 'hasBeenOverridden',
- 'user.name': defaultValue.user.name,
+ user: {
+ name: defaultValue.user.name,
+ },
});
});
});
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
index be4535fec36694..869d1fac54b1ea 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
@@ -58,8 +58,6 @@ export function useForm(
return initDefaultValue(defaultValue);
}, [defaultValue, initDefaultValue]);
- const defaultValueDeserialized = useRef(defaultValueMemoized);
-
const { valueChangeDebounceTime, stripEmptyFields: doStripEmptyFields } = options ?? {};
const formOptions = useMemo(
() => ({
@@ -72,26 +70,36 @@ export function useForm(
const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setSubmitting] = useState(false);
const [isValid, setIsValid] = useState(undefined);
+
const fieldsRefs = useRef({});
const formUpdateSubscribers = useRef([]);
const isMounted = useRef(false);
+ const defaultValueDeserialized = useRef(defaultValueMemoized);
// formData$ is an observable we can subscribe to in order to receive live
// update of the raw form data. As an observable it does not trigger any React
// render().
- // The component is the one in charge of reading this observable
- // and updating its state to trigger the necessary view render.
- const formData$ = useRef | null>(null);
+ // The "useFormData()" hook is the one in charge of reading this observable
+ // and updating its own state that will trigger the necessary re-renders in the UI.
+ const formData$ = useRef | null>(null);
+ // ----------------------------------
// -- HELPERS
// ----------------------------------
- const getFormData$ = useCallback((): Subject => {
+ const getFormData$ = useCallback((): Subject => {
if (formData$.current === null) {
- formData$.current = new Subject({} as T);
+ formData$.current = new Subject({});
}
return formData$.current;
}, []);
+ const updateFormData$ = useCallback(
+ (nextValue: FormData) => {
+ getFormData$().next(nextValue);
+ },
+ [getFormData$]
+ );
+
const fieldsToArray = useCallback<() => FieldHook[]>(() => Object.values(fieldsRefs.current), []);
const getFieldsForOutput = useCallback(
@@ -115,63 +123,24 @@ export function useForm(
[]
);
- const updateFormDataAt: FormHook['__updateFormDataAt'] = useCallback(
+ const updateFormDataAt: FormHook['__updateFormDataAt'] = useCallback(
(path, value) => {
- const _formData$ = getFormData$();
- const currentFormData = _formData$.value;
+ const currentFormData = getFormData$().value;
if (currentFormData[path] !== value) {
- _formData$.next({ ...currentFormData, [path]: value });
+ updateFormData$({ ...currentFormData, [path]: value });
}
-
- return _formData$.value;
},
- [getFormData$]
+ [getFormData$, updateFormData$]
);
- const updateDefaultValueAt: FormHook['__updateDefaultValueAt'] = useCallback((path, value) => {
- set(defaultValueDeserialized.current, path, value);
- }, []);
-
- // -- API
- // ----------------------------------
- const getFormData: FormHook['getFormData'] = useCallback(
- (getDataOptions: Parameters['getFormData']>[0] = { unflatten: true }) => {
- if (getDataOptions.unflatten) {
- const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, {
- stripEmptyFields: formOptions.stripEmptyFields,
- });
- const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue());
- return serializer
- ? (serializer(unflattenObject(fieldsValue) as I) as T)
- : (unflattenObject(fieldsValue) as T);
- }
-
- return Object.entries(fieldsRefs.current).reduce(
- (acc, [key, field]) => ({
- ...acc,
- [key]: field.value,
- }),
- {} as T
- );
+ const updateDefaultValueAt: FormHook['__updateDefaultValueAt'] = useCallback(
+ (path, value) => {
+ set(defaultValueDeserialized.current, path, value);
},
- [getFieldsForOutput, formOptions.stripEmptyFields, serializer]
+ []
);
- const getErrors: FormHook['getErrors'] = useCallback(() => {
- if (isValid === true) {
- return [];
- }
-
- return fieldsToArray().reduce((acc, field) => {
- const fieldError = field.getErrorsMessages();
- if (fieldError === null) {
- return acc;
- }
- return [...acc, fieldError];
- }, [] as string[]);
- }, [isValid, fieldsToArray]);
-
const isFieldValid = (field: FieldHook) => field.isValid && !field.isValidating;
const waitForFieldsToFinishValidating = useCallback(async () => {
@@ -192,13 +161,13 @@ export function useForm(
});
}, [fieldsToArray]);
- const validateFields: FormHook['__validateFields'] = useCallback(
+ const validateFields: FormHook['__validateFields'] = useCallback(
async (fieldNames) => {
const fieldsToValidate = fieldNames
.map((name) => fieldsRefs.current[name])
.filter((field) => field !== undefined);
- const formData = getFormData({ unflatten: false });
+ const formData = getFormData$().value;
const validationResult = await Promise.all(
fieldsToValidate.map((field) => field.validate({ formData }))
);
@@ -245,34 +214,13 @@ export function useForm(
return { areFieldsValid, isFormValid };
},
- [getFormData, fieldsToArray]
+ [getFormData$, fieldsToArray]
);
- const validateAllFields = useCallback(async (): Promise => {
- // Maybe some field are being validated because of their async validation(s).
- // We make sure those validations have finished executing before proceeding.
- await waitForFieldsToFinishValidating();
-
- if (!isMounted.current) {
- return false;
- }
-
- const fieldsArray = fieldsToArray();
- const fieldsToValidate = fieldsArray.filter((field) => !field.isValidated);
-
- let isFormValid: boolean | undefined;
-
- if (fieldsToValidate.length === 0) {
- isFormValid = fieldsArray.every(isFieldValid);
- } else {
- ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path)));
- }
-
- setIsValid(isFormValid);
- return isFormValid!;
- }, [fieldsToArray, validateFields, waitForFieldsToFinishValidating]);
-
- const addField: FormHook['__addField'] = useCallback(
+ // ----------------------------------
+ // -- Internal API
+ // ----------------------------------
+ const addField: FormHook['__addField'] = useCallback(
(field) => {
fieldsRefs.current[field.path] = field;
@@ -291,17 +239,17 @@ export function useForm(
[updateFormDataAt]
);
- const removeField: FormHook['__removeField'] = useCallback(
+ const removeField: FormHook['__removeField'] = useCallback(
(_fieldNames) => {
const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames];
- const currentFormData = { ...getFormData$().value } as FormData;
+ const currentFormData = { ...getFormData$().value };
fieldNames.forEach((name) => {
delete fieldsRefs.current[name];
delete currentFormData[name];
});
- getFormData$().next(currentFormData as T);
+ updateFormData$(currentFormData);
/**
* After removing a field, the form validity might have changed
@@ -316,40 +264,91 @@ export function useForm(
return prev;
});
},
- [getFormData$, fieldsToArray]
+ [getFormData$, updateFormData$, fieldsToArray]
+ );
+
+ const getFieldDefaultValue: FormHook['__getFieldDefaultValue'] = useCallback(
+ (fieldName) => get(defaultValueDeserialized.current, fieldName),
+ []
+ );
+
+ const readFieldConfigFromSchema: FormHook['__readFieldConfigFromSchema'] = useCallback(
+ (fieldName) => {
+ const config = (get(schema ?? {}, fieldName) as FieldConfig) || {};
+
+ return config;
+ },
+ [schema]
);
- const setFieldValue: FormHook['setFieldValue'] = useCallback((fieldName, value) => {
+ // ----------------------------------
+ // -- Public API
+ // ----------------------------------
+ const getFormData: FormHook['getFormData'] = useCallback(() => {
+ const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, {
+ stripEmptyFields: formOptions.stripEmptyFields,
+ });
+ const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue());
+ return serializer
+ ? serializer(unflattenObject(fieldsValue))
+ : unflattenObject(fieldsValue);
+ }, [getFieldsForOutput, formOptions.stripEmptyFields, serializer]);
+
+ const getErrors: FormHook['getErrors'] = useCallback(() => {
+ if (isValid === true) {
+ return [];
+ }
+
+ return fieldsToArray().reduce((acc, field) => {
+ const fieldError = field.getErrorsMessages();
+ if (fieldError === null) {
+ return acc;
+ }
+ return [...acc, fieldError];
+ }, [] as string[]);
+ }, [isValid, fieldsToArray]);
+
+ const validate: FormHook['validate'] = useCallback(async (): Promise => {
+ // Maybe some field are being validated because of their async validation(s).
+ // We make sure those validations have finished executing before proceeding.
+ await waitForFieldsToFinishValidating();
+
+ if (!isMounted.current) {
+ return false;
+ }
+
+ const fieldsArray = fieldsToArray();
+ const fieldsToValidate = fieldsArray.filter((field) => !field.isValidated);
+
+ let isFormValid: boolean | undefined;
+
+ if (fieldsToValidate.length === 0) {
+ isFormValid = fieldsArray.every(isFieldValid);
+ } else {
+ ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path)));
+ }
+
+ setIsValid(isFormValid);
+ return isFormValid!;
+ }, [fieldsToArray, validateFields, waitForFieldsToFinishValidating]);
+
+ const setFieldValue: FormHook['setFieldValue'] = useCallback((fieldName, value) => {
if (fieldsRefs.current[fieldName] === undefined) {
return;
}
fieldsRefs.current[fieldName].setValue(value);
}, []);
- const setFieldErrors: FormHook['setFieldErrors'] = useCallback((fieldName, errors) => {
+ const setFieldErrors: FormHook['setFieldErrors'] = useCallback((fieldName, errors) => {
if (fieldsRefs.current[fieldName] === undefined) {
return;
}
fieldsRefs.current[fieldName].setErrors(errors);
}, []);
- const getFields: FormHook['getFields'] = useCallback(() => fieldsRefs.current, []);
-
- const getFieldDefaultValue: FormHook['__getFieldDefaultValue'] = useCallback(
- (fieldName) => get(defaultValueDeserialized.current, fieldName),
- []
- );
-
- const readFieldConfigFromSchema: FormHook['__readFieldConfigFromSchema'] = useCallback(
- (fieldName) => {
- const config = (get(schema ?? {}, fieldName) as FieldConfig) || {};
+ const getFields: FormHook['getFields'] = useCallback(() => fieldsRefs.current, []);
- return config;
- },
- [schema]
- );
-
- const submitForm: FormHook['submit'] = useCallback(
+ const submit: FormHook['submit'] = useCallback(
async (e) => {
if (e) {
e.preventDefault();
@@ -358,7 +357,7 @@ export function useForm(
setIsSubmitted(true); // User has attempted to submit the form at least once
setSubmitting(true);
- const isFormValid = await validateAllFields();
+ const isFormValid = await validate();
const formData = isFormValid ? getFormData() : ({} as T);
if (onSubmit) {
@@ -371,13 +370,17 @@ export function useForm(
return { data: formData, isValid: isFormValid! };
},
- [validateAllFields, getFormData, onSubmit]
+ [validate, getFormData, onSubmit]
);
- const subscribe: FormHook['subscribe'] = useCallback(
+ const subscribe: FormHook['subscribe'] = useCallback(
(handler) => {
const subscription = getFormData$().subscribe((raw) => {
- handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields });
+ handler({
+ isValid,
+ data: { internal: unflattenObject(raw), format: getFormData },
+ validate,
+ });
});
formUpdateSubscribers.current.push(subscription);
@@ -391,17 +394,13 @@ export function useForm(
},
};
},
- [getFormData$, isValid, getFormData, validateAllFields]
+ [getFormData$, isValid, getFormData, validate]
);
- /**
- * Reset all the fields of the form to their default values
- * and reset all the states to their original value.
- */
- const reset: FormHook['reset'] = useCallback(
+ const reset: FormHook['reset'] = useCallback(
(resetOptions = { resetValues: true }) => {
const { resetValues = true, defaultValue: updatedDefaultValue } = resetOptions;
- const currentFormData = { ...getFormData$().value } as FormData;
+ const currentFormData = { ...getFormData$().value };
if (updatedDefaultValue) {
defaultValueDeserialized.current = initDefaultValue(updatedDefaultValue);
@@ -417,25 +416,26 @@ export function useForm(
currentFormData[path] = fieldDefaultValue;
}
});
+
if (resetValues) {
- getFormData$().next(currentFormData as T);
+ updateFormData$(currentFormData);
}
setIsSubmitted(false);
setSubmitting(false);
setIsValid(undefined);
},
- [getFormData$, initDefaultValue, getFieldDefaultValue]
+ [getFormData$, updateFormData$, initDefaultValue, getFieldDefaultValue]
);
- const form = useMemo>(() => {
+ const form = useMemo>(() => {
return {
isSubmitted,
isSubmitting,
isValid,
id,
- submit: submitForm,
- validate: validateAllFields,
+ submit,
+ validate,
subscribe,
setFieldValue,
setFieldErrors,
@@ -458,7 +458,7 @@ export function useForm(
isSubmitting,
isValid,
id,
- submitForm,
+ submit,
subscribe,
setFieldValue,
setFieldErrors,
@@ -475,7 +475,7 @@ export function useForm(
addField,
removeField,
validateFields,
- validateAllFields,
+ validate,
]);
useEffect(() => {
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx
index 0fb65daecf2f4f..beb8e58edbf491 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import React, { useEffect } from 'react';
+import React, { useEffect, useRef } from 'react';
import { act } from 'react-dom/test-utils';
import { registerTestBed, TestBed } from '../shared_imports';
@@ -25,37 +25,59 @@ import { Form, UseField } from '../components';
import { useForm } from './use_form';
import { useFormData, HookReturn } from './use_form_data';
-interface Props {
- onChange(data: HookReturn): void;
+interface Props {
+ onChange(data: HookReturn): void;
watch?: string | string[];
}
+interface Form1 {
+ title: string;
+}
+
+interface Form2 {
+ user: {
+ firstName: string;
+ lastName: string;
+ };
+}
+
+interface Form3 {
+ title: string;
+ subTitle: string;
+}
+
describe('useFormData() hook', () => {
- const HookListenerComp = React.memo(({ onChange, watch }: Props) => {
- const hookValue = useFormData({ watch });
+ const HookListenerComp = function ({ onChange, watch }: Props) {
+ const hookValue = useFormData({ watch });
+ const isMounted = useRef(false);
useEffect(() => {
- onChange(hookValue);
+ if (isMounted.current) {
+ onChange(hookValue);
+ }
+ isMounted.current = true;
}, [hookValue, onChange]);
return null;
- });
+ };
+
+ const HookListener = React.memo(HookListenerComp);
describe('form data updates', () => {
let testBed: TestBed;
let onChangeSpy: jest.Mock;
const getLastMockValue = () => {
- return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
+ return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
};
- const TestComp = (props: Props) => {
- const { form } = useForm();
+ const TestComp = (props: Props) => {
+ const { form } = useForm();
return (
);
};
@@ -70,9 +92,7 @@ describe('useFormData() hook', () => {
});
test('should return the form data', () => {
- // Called twice:
- // once when the hook is called and once when the fields have mounted and updated the form data
- expect(onChangeSpy).toBeCalledTimes(2);
+ expect(onChangeSpy).toBeCalledTimes(1);
const [data] = getLastMockValue();
expect(data).toEqual({ title: 'titleInitialValue' });
});
@@ -86,7 +106,7 @@ describe('useFormData() hook', () => {
setInputValue('titleField', 'titleChanged');
});
- expect(onChangeSpy).toBeCalledTimes(3);
+ expect(onChangeSpy).toBeCalledTimes(2);
const [data] = getLastMockValue();
expect(data).toEqual({ title: 'titleChanged' });
});
@@ -96,17 +116,17 @@ describe('useFormData() hook', () => {
let onChangeSpy: jest.Mock;
const getLastMockValue = () => {
- return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
+ return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
};
- const TestComp = (props: Props) => {
- const { form } = useForm();
+ const TestComp = (props: Props) => {
+ const { form } = useForm();
return (
);
};
@@ -121,8 +141,8 @@ describe('useFormData() hook', () => {
});
test('should expose a handler to build the form data', () => {
- const { 1: format } = getLastMockValue();
- expect(format()).toEqual({
+ const [formData] = getLastMockValue();
+ expect(formData).toEqual({
user: {
firstName: 'John',
lastName: 'Snow',
@@ -137,11 +157,11 @@ describe('useFormData() hook', () => {
let onChangeSpy: jest.Mock;
const getLastMockValue = () => {
- return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
+ return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
};
- const TestComp = (props: Props) => {
- const { form } = useForm();
+ const TestComp = (props: Props) => {
+ const { form } = useForm();
return (
-
-
-
-
- ),
- endpointSecurityData: (
-
-
-
- ),
- }}
- />
-
-
- );
+ renderDescription = () => {
+ const { isSecurityExampleEnabled } = this.props;
+ const securityExampleEnabled = isSecurityExampleEnabled();
+ const clusterDataLink = (
+
+
+
+ );
+
+ const endpointSecurityDataLink = (
+
+
+
+ );
+
+ return (
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+ {securityExampleEnabled ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ };
toggleOptIn = async (): Promise => {
const { telemetryService, toasts } = this.props;
@@ -264,6 +283,9 @@ export class TelemetryManagementSection extends Component {
};
toggleSecurityExample = () => {
+ const { isSecurityExampleEnabled } = this.props;
+ const securityExampleEnabled = isSecurityExampleEnabled();
+ if (!securityExampleEnabled) return;
this.setState({
showSecurityExample: !this.state.showSecurityExample,
});
diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx
index f61268c4772a3d..95acbaba38845e 100644
--- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx
+++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx
@@ -28,13 +28,15 @@ type Props = any;
const TelemetryManagementSectionComponent = lazy(() => import('./telemetry_management_section'));
export function telemetryManagementSectionWrapper(
- telemetryService: TelemetryPluginSetup['telemetryService']
+ telemetryService: TelemetryPluginSetup['telemetryService'],
+ shouldShowSecuritySolutionUsageExample: () => boolean
) {
const TelemetryManagementSectionWrapper = (props: Props) => (
}>
diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts
index 082f68809a67e5..f3aef9eca750d8 100644
--- a/src/plugins/telemetry_management_section/public/index.ts
+++ b/src/plugins/telemetry_management_section/public/index.ts
@@ -16,9 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
-export { OptInExampleFlyout } from './components';
import { TelemetryManagementSectionPlugin } from './plugin';
+export { OptInExampleFlyout } from './components';
+
+export { TelemetryManagementSectionPluginSetup } from './plugin';
export function plugin() {
return new TelemetryManagementSectionPlugin();
}
diff --git a/src/plugins/telemetry_management_section/public/plugin.tsx b/src/plugins/telemetry_management_section/public/plugin.tsx
index 738b38c36d30d0..c200e830c8f612 100644
--- a/src/plugins/telemetry_management_section/public/plugin.tsx
+++ b/src/plugins/telemetry_management_section/public/plugin.tsx
@@ -38,16 +38,32 @@ export interface TelemetryManagementSectionPluginDepsSetup {
advancedSettings: AdvancedSettingsSetup;
}
-export class TelemetryManagementSectionPlugin implements Plugin {
+export interface TelemetryManagementSectionPluginSetup {
+ toggleSecuritySolutionExample: (enabled: boolean) => void;
+}
+
+export class TelemetryManagementSectionPlugin
+ implements Plugin {
+ private showSecuritySolutionExample = false;
+ private shouldShowSecuritySolutionExample = () => {
+ return this.showSecuritySolutionExample;
+ };
+
public setup(
core: CoreSetup,
{ advancedSettings, telemetry: { telemetryService } }: TelemetryManagementSectionPluginDepsSetup
) {
advancedSettings.component.register(
advancedSettings.component.componentType.PAGE_FOOTER_COMPONENT,
- telemetryManagementSectionWrapper(telemetryService),
+ telemetryManagementSectionWrapper(telemetryService, this.shouldShowSecuritySolutionExample),
true
);
+
+ return {
+ toggleSecuritySolutionExample: (enabled: boolean) => {
+ this.showSecuritySolutionExample = enabled;
+ },
+ };
}
public start(core: CoreStart) {}
diff --git a/src/plugins/usage_collection/public/plugin.ts b/src/plugins/usage_collection/public/plugin.ts
index 40f27f82699288..79faa9a102909f 100644
--- a/src/plugins/usage_collection/public/plugin.ts
+++ b/src/plugins/usage_collection/public/plugin.ts
@@ -30,7 +30,7 @@ import {
} from '../../../core/public';
import { reportApplicationUsage } from './services/application_usage';
-interface PublicConfigType {
+export interface PublicConfigType {
uiMetric: {
enabled: boolean;
debug: boolean;
diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json
new file mode 100644
index 00000000000000..96b2c4d37e17c2
--- /dev/null
+++ b/src/plugins/usage_collection/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": [
+ "public/**/*",
+ "server/**/*",
+ "common/*",
+ "../../../typings/**/*"
+ ],
+ "references": [
+ { "path": "../../core/tsconfig.json" },
+ { "path": "../../plugins/kibana_utils/tsconfig.json" }
+ ]
+}
diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap
index e32425a0954291..88ed7c66a79a2b 100644
--- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap
+++ b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`tag cloud tests tagcloudscreenshot should render simple image 1`] = `"foo bar foobar "`;
+exports[`tag cloud tests tagcloudscreenshot should render simple image 1`] = `"foo bar foobar "`;
diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap
index dbc3dd1202cbd2..d7707f64d8a4fc 100644
--- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap
+++ b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`TagCloudVisualizationTest TagCloudVisualization - basics simple draw 1`] = `"CN IN US DE BR "`;
+exports[`TagCloudVisualizationTest TagCloudVisualization - basics simple draw 1`] = `"CN IN US DE BR "`;
-exports[`TagCloudVisualizationTest TagCloudVisualization - basics with param change 1`] = `"CN IN US DE BR "`;
+exports[`TagCloudVisualizationTest TagCloudVisualization - basics with param change 1`] = `"CN IN US DE BR "`;
-exports[`TagCloudVisualizationTest TagCloudVisualization - basics with resize 1`] = `"CN IN US DE BR "`;
+exports[`TagCloudVisualizationTest TagCloudVisualization - basics with resize 1`] = `"CN IN US DE BR "`;
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js
index e48515d2438442..b1de60f854a1c7 100644
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js
@@ -48,7 +48,11 @@ export class TagCloud extends EventEmitter {
this.resize();
//SETTING (non-configurable)
- this._fontFamily = 'Open Sans, sans-serif';
+ /**
+ * the fontFamily should be set explicitly for calculating a layout
+ * and to avoid words overlapping
+ */
+ this._fontFamily = 'Inter UI, sans-serif';
this._fontStyle = 'normal';
this._fontWeight = 'normal';
this._spiral = 'archimedean'; //layout shape
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx
index 18a09ec9f49698..cb0daa6d293825 100644
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx
@@ -62,10 +62,10 @@ export const TagCloudChart = ({
() =>
throttle(() => {
if (visController.current) {
- visController.current.render().then(renderComplete);
+ visController.current.render(visData, visParams).then(renderComplete);
}
}, 300),
- [renderComplete]
+ [renderComplete, visData, visParams]
);
return (
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js
index 5ec22d2c6a4d9e..0d64c9d02eafd0 100644
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js
@@ -85,11 +85,8 @@ export class TagCloudVisualization {
}
async render(data, visParams) {
- if (data && visParams) {
- this._updateParams(visParams);
- this._updateData(data);
- }
-
+ this._updateParams(visParams);
+ this._updateData(data);
this._resize();
await this._renderComplete$.pipe(take(1)).toPromise();
diff --git a/test/tsconfig.json b/test/tsconfig.json
index 2c92367f84823e..ec6612d0dd00dc 100644
--- a/test/tsconfig.json
+++ b/test/tsconfig.json
@@ -20,6 +20,9 @@
"references": [
{ "path": "../src/core/tsconfig.json" },
{ "path": "../src/plugins/kibana_utils/tsconfig.json" },
- { "path": "../src/plugins/kibana_react/tsconfig.json" }
+ { "path": "../src/plugins/kibana_react/tsconfig.json" },
+ { "path": "../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" },
+ { "path": "../src/plugins/telemetry/tsconfig.json" }
]
}
diff --git a/tsconfig.json b/tsconfig.json
index cf112b26a2cbbd..4f22168b67a095 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,18 +3,17 @@
"compilerOptions": {
"tsBuildInfoFile": "./build/tsbuildinfo/kibana"
},
- "include": [
- "kibana.d.ts",
- "src/**/*",
- "typings/**/*",
- "test_utils/**/*"
- ],
+ "include": ["kibana.d.ts", "src/**/*", "typings/**/*", "test_utils/**/*"],
"exclude": [
"src/**/__fixtures__/**/*",
"src/test_utils/**/*",
"src/core/**/*",
+ "src/plugins/kibana_legacy/**/*",
"src/plugins/kibana_utils/**/*",
- "src/plugins/kibana_react/**/*"
+ "src/plugins/kibana_react/**/*",
+ "src/plugins/usage_collection/**/*",
+ "src/plugins/telemetry_collection_manager/**/*",
+ "src/plugins/telemetry/**/*"
// In the build we actually exclude **/public/**/* from this config so that
// we can run the TSC on both this and the .browser version of this config
// file, but if we did it during development IDEs would not be able to find
@@ -25,6 +24,9 @@
{ "path": "./src/test_utils/tsconfig.json" },
{ "path": "./src/core/tsconfig.json" },
{ "path": "./src/plugins/kibana_utils/tsconfig.json" },
- { "path": "./src/plugins/kibana_react/tsconfig.json" }
+ { "path": "./src/plugins/kibana_react/tsconfig.json" },
+ { "path": "./src/plugins/usage_collection/tsconfig.json" },
+ { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" },
+ { "path": "./src/plugins/telemetry/tsconfig.json" }
]
}
diff --git a/tsconfig.refs.json b/tsconfig.refs.json
index 54bd07e4b134ce..4b6ed4a0db8135 100644
--- a/tsconfig.refs.json
+++ b/tsconfig.refs.json
@@ -5,5 +5,8 @@
{ "path": "./src/core/tsconfig.json" },
{ "path": "./src/plugins/kibana_utils/tsconfig.json" },
{ "path": "./src/plugins/kibana_react/tsconfig.json" },
+ { "path": "./src/plugins/usage_collection/tsconfig.json" },
+ { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" },
+ { "path": "./src/plugins/telemetry/tsconfig.json" },
]
}
diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts
index e641b81189b93a..95e7784e51acf5 100644
--- a/x-pack/plugins/actions/server/action_type_registry.test.ts
+++ b/x-pack/plugins/actions/server/action_type_registry.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../task_manager/server/mocks';
import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_registry';
import { ActionType, ExecutorType } from './types';
import { ActionExecutor, ExecutorError, ILicenseState, TaskRunnerFactory } from './lib';
@@ -13,7 +13,7 @@ import { licenseStateMock } from './lib/license_state.mock';
import { ActionsConfigurationUtilities } from './actions_config';
import { licensingMock } from '../../licensing/server/mocks';
-const mockTaskManager = taskManagerMock.setup();
+const mockTaskManager = taskManagerMock.createSetup();
let mockedLicenseState: jest.Mocked;
let mockedActionsConfig: jest.Mocked;
let actionTypeRegistryParams: ActionTypeRegistryOpts;
@@ -66,7 +66,6 @@ describe('register()', () => {
"getRetry": [Function],
"maxAttempts": 1,
"title": "My action type",
- "type": "actions:my-action-type",
},
},
]
diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts
index b93d4a6e78ac66..cacf7166b96ba3 100644
--- a/x-pack/plugins/actions/server/action_type_registry.ts
+++ b/x-pack/plugins/actions/server/action_type_registry.ts
@@ -125,7 +125,6 @@ export class ActionTypeRegistry {
this.taskManager.registerTaskDefinitions({
[`actions:${actionType.id}`]: {
title: actionType.name,
- type: `actions:${actionType.id}`,
maxAttempts: actionType.maxAttempts || 1,
getRetry(attempts: number, error: unknown) {
if (error instanceof ExecutorError) {
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index 2b6aec42e0d210..171f8d4b0b1d43 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -10,7 +10,7 @@ import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_regist
import { ActionsClient } from './actions_client';
import { ExecutorType, ActionType } from './types';
import { ActionExecutor, TaskRunnerFactory, ILicenseState } from './lib';
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../task_manager/server/mocks';
import { actionsConfigMock } from './actions_config.mock';
import { getActionsConfigurationUtilities } from './actions_config';
import { licenseStateMock } from './lib/license_state.mock';
@@ -34,7 +34,7 @@ const authorization = actionsAuthorizationMock.create();
const executionEnqueuer = jest.fn();
const request = {} as KibanaRequest;
-const mockTaskManager = taskManagerMock.setup();
+const mockTaskManager = taskManagerMock.createSetup();
let actionsClient: ActionsClient;
let mockedLicenseState: jest.Mocked;
diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts
index f7882849708e59..a9d1e28182b291 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts
@@ -6,7 +6,7 @@
import { ActionExecutor, TaskRunnerFactory } from '../lib';
import { ActionTypeRegistry } from '../action_type_registry';
-import { taskManagerMock } from '../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../task_manager/server/mocks';
import { registerBuiltInActionTypes } from './index';
import { Logger } from '../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
@@ -22,8 +22,8 @@ export function createActionTypeRegistry(): {
} {
const logger = loggingSystemMock.create().get() as jest.Mocked;
const actionTypeRegistry = new ActionTypeRegistry({
+ taskManager: taskManagerMock.createSetup(),
licensing: licensingMock.createSetup(),
- taskManager: taskManagerMock.setup(),
taskRunnerFactory: new TaskRunnerFactory(
new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false })
),
diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts
index 33e78ee444cd06..ed06bd888f9199 100644
--- a/x-pack/plugins/actions/server/create_execute_function.test.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.test.ts
@@ -6,7 +6,7 @@
import { KibanaRequest } from 'src/core/server';
import uuid from 'uuid';
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../task_manager/server/mocks';
import { createExecutionEnqueuerFunction } from './create_execute_function';
import { savedObjectsClientMock } from '../../../../src/core/server/mocks';
import { actionTypeRegistryMock } from './action_type_registry.mock';
@@ -15,7 +15,7 @@ import {
asSavedObjectExecutionSource,
} from './lib/action_execution_source';
-const mockTaskManager = taskManagerMock.start();
+const mockTaskManager = taskManagerMock.createStart();
const savedObjectsClient = savedObjectsClientMock.create();
const request = {} as KibanaRequest;
diff --git a/x-pack/plugins/actions/server/feature.ts b/x-pack/plugins/actions/server/feature.ts
index 225f2761bad427..3ec824f955e724 100644
--- a/x-pack/plugins/actions/server/feature.ts
+++ b/x-pack/plugins/actions/server/feature.ts
@@ -13,8 +13,6 @@ export const ACTIONS_FEATURE = {
name: i18n.translate('xpack.actions.featureRegistry.actionsFeatureName', {
defaultMessage: 'Actions and Connectors',
}),
- icon: 'bell',
- navLinkId: 'actions',
category: DEFAULT_APP_CATEGORIES.management,
app: [],
management: {
diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts
index e2f11abeefff22..ad1c51d06d0c08 100644
--- a/x-pack/plugins/actions/server/mocks.ts
+++ b/x-pack/plugins/actions/server/mocks.ts
@@ -45,6 +45,7 @@ const createServicesMock = () => {
callCluster: elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser,
getLegacyScopedClusterClient: jest.fn(),
savedObjectsClient: savedObjectsClientMock.create(),
+ scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient().asCurrentUser,
};
return mock;
};
diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts
index ef20ffbb9ee68a..668e8b849b8a72 100644
--- a/x-pack/plugins/actions/server/plugin.ts
+++ b/x-pack/plugins/actions/server/plugin.ts
@@ -426,6 +426,7 @@ export class ActionsPlugin implements Plugin, Plugi
return (request) => ({
callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser,
savedObjectsClient: getScopedClient(request),
+ scopedClusterClient: elasticsearch.client.asScoped(request).asCurrentUser,
getLegacyScopedClusterClient(clusterClient: ILegacyClusterClient) {
return clusterClient.asScoped(request);
},
diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts
index a23a2b08932613..a8db8bfd7344cc 100644
--- a/x-pack/plugins/actions/server/types.ts
+++ b/x-pack/plugins/actions/server/types.ts
@@ -14,6 +14,7 @@ import {
KibanaRequest,
SavedObjectsClientContract,
SavedObjectAttributes,
+ ElasticsearchClient,
} from '../../../../src/core/server';
import { ActionTypeExecutorResult } from '../common';
export { ActionTypeExecutorResult } from '../common';
@@ -30,6 +31,7 @@ export type ActionTypeParams = Record;
export interface Services {
callCluster: ILegacyScopedClusterClient['callAsCurrentUser'];
savedObjectsClient: SavedObjectsClientContract;
+ scopedClusterClient: ElasticsearchClient;
getLegacyScopedClusterClient(clusterClient: ILegacyClusterClient): ILegacyScopedClusterClient;
}
diff --git a/x-pack/plugins/actions/server/usage/actions_usage_collector.test.ts b/x-pack/plugins/actions/server/usage/actions_usage_collector.test.ts
index 2e2944aab425cc..0e6c2ff37eb029 100644
--- a/x-pack/plugins/actions/server/usage/actions_usage_collector.test.ts
+++ b/x-pack/plugins/actions/server/usage/actions_usage_collector.test.ts
@@ -6,9 +6,9 @@
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { registerActionsUsageCollector } from './actions_usage_collector';
-import { taskManagerMock } from '../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../task_manager/server/mocks';
-const mockTaskManagerStart = taskManagerMock.start();
+const mockTaskManagerStart = taskManagerMock.createStart();
beforeEach(() => jest.resetAllMocks());
diff --git a/x-pack/plugins/actions/server/usage/task.ts b/x-pack/plugins/actions/server/usage/task.ts
index efa695cdc2667b..f7af480aa9fb36 100644
--- a/x-pack/plugins/actions/server/usage/task.ts
+++ b/x-pack/plugins/actions/server/usage/task.ts
@@ -39,7 +39,6 @@ function registerActionsTelemetryTask(
taskManager.registerTaskDefinitions({
[TELEMETRY_TASK_TYPE]: {
title: 'Actions usage fetch task',
- type: TELEMETRY_TASK_TYPE,
timeout: '5m',
createTaskRunner: telemetryTaskRunner(logger, core, kibanaIndex),
},
diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts
index 048cc3d5a44402..9e1545bae53845 100644
--- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts
+++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts
@@ -7,9 +7,9 @@
import { TaskRunnerFactory } from './task_runner';
import { AlertTypeRegistry } from './alert_type_registry';
import { AlertType } from './types';
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../task_manager/server/mocks';
-const taskManager = taskManagerMock.setup();
+const taskManager = taskManagerMock.createSetup();
const alertTypeRegistryParams = {
taskManager,
taskRunnerFactory: new TaskRunnerFactory(),
@@ -118,7 +118,6 @@ describe('register()', () => {
"alerting:test": Object {
"createTaskRunner": [Function],
"title": "Test",
- "type": "alerting:test",
},
},
]
diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts
index 7f34803b05a810..0cd218571035ab 100644
--- a/x-pack/plugins/alerts/server/alert_type_registry.ts
+++ b/x-pack/plugins/alerts/server/alert_type_registry.ts
@@ -86,7 +86,6 @@ export class AlertTypeRegistry {
this.taskManager.registerTaskDefinitions({
[`alerting:${alertType.id}`]: {
title: alertType.name,
- type: `alerting:${alertType.id}`,
createTaskRunner: (context: RunContext) =>
this.taskRunnerFactory.create({ ...alertType } as AlertType, context),
},
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
index 56e868732e3fb4..bce1af203fb0e3 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
@@ -6,7 +6,7 @@
import { schema } from '@kbn/config-schema';
import { AlertsClient, ConstructorOptions, CreateOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -16,7 +16,7 @@ import { ActionsAuthorization, ActionsClient } from '../../../../actions/server'
import { TaskStatus } from '../../../../task_manager/server';
import { getBeforeSetup, setGlobalDate } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts
index 1ebd9fc296b13f..d9b253c3a56e81 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -14,7 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
index 2dd3da07234ce3..d0557df6220284 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -14,7 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
index b214d8ba697b1d..f098bbcad8d050 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -15,7 +15,7 @@ import { ActionsAuthorization } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
import { getBeforeSetup } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
index bf55a2070d8fe2..c1adaddc80d9e0 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { nodeTypes } from '../../../../../../src/plugins/data/common';
@@ -16,7 +16,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup, setGlobalDate } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
index 327a1fa23ef05d..004230403de2e8 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -14,7 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup, setGlobalDate } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts
index 09212732b76e79..a53e49337f3857 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -19,7 +19,7 @@ import { EventsFactory } from '../../lib/alert_instance_summary_from_event_log.t
import { RawAlert } from '../../types';
import { getBeforeSetup, mockedDateString, setGlobalDate } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const eventLogClient = eventLogClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts
index 42e573aea347f0..8b32f05f6d5a1f 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { TaskStatus } from '../../../../task_manager/server';
@@ -15,7 +15,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts
index 96e49e21b90459..5ebb4e90d4b508 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts
@@ -3,8 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { TaskManager } from '../../../../task_manager/server/task_manager';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { IEventLogClient } from '../../../../event_log/server';
import { actionsClientMock } from '../../../../actions/server/mocks';
import { ConstructorOptions } from '../alerts_client';
@@ -41,9 +40,7 @@ export function setGlobalDate() {
export function getBeforeSetup(
alertsClientParams: jest.Mocked,
- taskManager: jest.Mocked<
- Pick
- >,
+ taskManager: ReturnType,
alertTypeRegistry: jest.Mocked>,
eventLogClient?: jest.Mocked
) {
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts
index 4337ed6c491d4b..b2f5c5498f848c 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -14,7 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
index 44ee6713f2560c..88199dfd1f7b9a 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -14,7 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
index dc9a1600a57764..cd7112b3551b36 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -14,7 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
index 45920db105c2aa..07666c1cc6261e 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -14,7 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
index 56040115011301..97711b8c14579a 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -14,7 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts
index 60b5b62954f05d..1dcde6addb9bfb 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts
@@ -7,7 +7,7 @@ import uuid from 'uuid';
import { schema } from '@kbn/config-schema';
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { IntervalSchedule } from '../../types';
@@ -19,7 +19,7 @@ import { ActionsAuthorization, ActionsClient } from '../../../../actions/server'
import { TaskStatus } from '../../../../task_manager/server';
import { getBeforeSetup, setGlobalDate } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts
index 97ddfa5e4adb46..1f3b567b2c0312 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
@@ -14,7 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
diff --git a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts
index 1c5edb45c80fe3..b1ac5ac4c6783e 100644
--- a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts
@@ -8,7 +8,7 @@ import { cloneDeep } from 'lodash';
import { AlertsClient, ConstructorOptions } from './alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../task_manager/server/mocks';
import { alertTypeRegistryMock } from './alert_type_registry.mock';
import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock';
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
@@ -25,7 +25,7 @@ const MockAlertId = 'alert-id';
const ConflictAfterRetries = RetryForConflictsAttempts + 1;
-const taskManager = taskManagerMock.start();
+const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts
index d747efbb959d8e..55c2f3ddd18a41 100644
--- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts
@@ -7,7 +7,7 @@
import { Request } from 'hapi';
import { AlertsClientFactory, AlertsClientFactoryOpts } from './alerts_client_factory';
import { alertTypeRegistryMock } from './alert_type_registry.mock';
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../task_manager/server/mocks';
import { KibanaRequest } from '../../../../src/core/server';
import {
savedObjectsClientMock,
@@ -35,7 +35,7 @@ const features = featuresPluginMock.createStart();
const securityPluginSetup = securityMock.createSetup();
const alertsClientFactoryParams: jest.Mocked = {
logger: loggingSystemMock.create().get(),
- taskManager: taskManagerMock.start(),
+ taskManager: taskManagerMock.createStart(),
alertTypeRegistry: alertTypeRegistryMock.create(),
getSpaceId: jest.fn(),
getSpace: jest.fn(),
diff --git a/x-pack/plugins/alerts/server/mocks.ts b/x-pack/plugins/alerts/server/mocks.ts
index c39aa13b580fcd..05d64bdbb77f4f 100644
--- a/x-pack/plugins/alerts/server/mocks.ts
+++ b/x-pack/plugins/alerts/server/mocks.ts
@@ -61,6 +61,7 @@ const createAlertServicesMock = () => {
callCluster: elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser,
getLegacyScopedClusterClient: jest.fn(),
savedObjectsClient: savedObjectsClientMock.create(),
+ scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient().asCurrentUser,
};
};
export type AlertServicesMock = ReturnType;
diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts
index 4cdcac4c9e889f..03302d5e6e7db3 100644
--- a/x-pack/plugins/alerts/server/plugin.ts
+++ b/x-pack/plugins/alerts/server/plugin.ts
@@ -302,6 +302,7 @@ export class AlertingPlugin {
return (request) => ({
callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser,
savedObjectsClient: this.getScopedClientWithAlertSavedObjectType(savedObjects, request),
+ scopedClusterClient: elasticsearch.client.asScoped(request).asCurrentUser,
getLegacyScopedClusterClient(clusterClient: ILegacyClusterClient) {
return clusterClient.asScoped(request);
},
diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts
index 03d41724213ce6..74153d1ca6b1df 100644
--- a/x-pack/plugins/alerts/server/types.ts
+++ b/x-pack/plugins/alerts/server/types.ts
@@ -10,6 +10,7 @@ import { PluginSetupContract, PluginStartContract } from './plugin';
import { AlertsClient } from './alerts_client';
export * from '../common';
import {
+ ElasticsearchClient,
ILegacyClusterClient,
ILegacyScopedClusterClient,
KibanaRequest,
@@ -45,6 +46,7 @@ declare module 'src/core/server' {
export interface Services {
callCluster: ILegacyScopedClusterClient['callAsCurrentUser'];
savedObjectsClient: SavedObjectsClientContract;
+ scopedClusterClient: ElasticsearchClient;
getLegacyScopedClusterClient(clusterClient: ILegacyClusterClient): ILegacyScopedClusterClient;
}
diff --git a/x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts
index b48d173ba36d9c..a5f83bc393d4ec 100644
--- a/x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts
+++ b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts
@@ -6,8 +6,8 @@
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { registerAlertsUsageCollector } from './alerts_usage_collector';
-import { taskManagerMock } from '../../../task_manager/server/task_manager.mock';
-const taskManagerStart = taskManagerMock.start();
+import { taskManagerMock } from '../../../task_manager/server/mocks';
+const taskManagerStart = taskManagerMock.createStart();
beforeEach(() => jest.resetAllMocks());
diff --git a/x-pack/plugins/alerts/server/usage/task.ts b/x-pack/plugins/alerts/server/usage/task.ts
index daf3ac246adadb..24ac15bbea78c5 100644
--- a/x-pack/plugins/alerts/server/usage/task.ts
+++ b/x-pack/plugins/alerts/server/usage/task.ts
@@ -42,7 +42,6 @@ function registerAlertingTelemetryTask(
taskManager.registerTaskDefinitions({
[TELEMETRY_TASK_TYPE]: {
title: 'Alerting usage fetch task',
- type: TELEMETRY_TASK_TYPE,
timeout: '5m',
createTaskRunner: telemetryTaskRunner(logger, core, kibanaIndex),
},
diff --git a/x-pack/plugins/apm/e2e/README.md b/x-pack/plugins/apm/e2e/README.md
index cf29b14d19541f..3517c74e950c71 100644
--- a/x-pack/plugins/apm/e2e/README.md
+++ b/x-pack/plugins/apm/e2e/README.md
@@ -2,8 +2,14 @@
**Run E2E tests**
+
```sh
+# In one terminal
+node ./scripts/kibana --no-base-path --dev --no-dev-config --config x-pack/plugins/apm/e2e/ci/kibana.e2e.yml
+# In another terminal
x-pack/plugins/apm/e2e/run-e2e.sh
```
-_Starts APM Server, Elasticsearch (with sample data) and runs the tests_
+Starts kibana, APM Server, Elasticsearch (with sample data) and runs the tests.
+
+If you see errors about not all events being ingested correctly try running `cd kibana/x-pack/plugins/apm/e2e/tmp/apm-integration-testing && docker-compose down -v`
diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts
index 55c980d5edeb40..314254883b2fd0 100644
--- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts
+++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts
@@ -12,9 +12,7 @@ When('the user changes the selected percentile', () => {
// wait for all loading to finish
cy.get('kbnLoadingIndicator').should('not.be.visible');
- getDataTestSubj('uxPercentileSelect').click();
-
- getDataTestSubj('p95Percentile').click();
+ getDataTestSubj('uxPercentileSelect').select('95');
});
Then(`it displays client metric related to that percentile`, () => {
@@ -22,8 +20,5 @@ Then(`it displays client metric related to that percentile`, () => {
verifyClientMetrics(metrics, false);
- // reset to median
- getDataTestSubj('uxPercentileSelect').click();
-
- getDataTestSubj('p50Percentile').click();
+ getDataTestSubj('uxPercentileSelect').select('50');
});
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
index 310c01291aea49..0b4dcea5d12e00 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
@@ -22,6 +22,24 @@ const ClFlexGroup = styled(EuiFlexGroup)`
}
`;
+function formatTitle(unit: string, value?: number) {
+ if (typeof value === 'undefined') return I18LABELS.dataMissing;
+ return formatToSec(value, unit);
+}
+
+function PageViewsTotalTitle({ pageViews }: { pageViews?: number }) {
+ if (typeof pageViews === 'undefined') {
+ return <>{I18LABELS.dataMissing}>;
+ }
+ return pageViews < 10000 ? (
+ <>{numeral(pageViews).format('0,0')}>
+ ) : (
+
+ <>{numeral(pageViews).format('0 a')}>
+
+ );
+}
+
export function ClientMetrics() {
const uxQuery = useUxQuery();
@@ -50,14 +68,12 @@ export function ClientMetrics() {
const STAT_STYLE = { width: '240px' };
- const pageViewsTotal = data?.pageViews?.value ?? 0;
-
return (
@@ -65,7 +81,7 @@ export function ClientMetrics() {
@@ -73,15 +89,7 @@ export function ClientMetrics() {
- <>{numeral(pageViewsTotal).format('0 a')}>
-
- )
- }
+ title={ }
description={I18LABELS.pageViews}
isLoading={status !== 'success'}
/>
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx
index f30a3ea5fb2dd0..793c9619edb3db 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx
@@ -8,6 +8,7 @@ import React from 'react';
import { EuiFlexItem, EuiStat, EuiFlexGroup } from '@elastic/eui';
import numeral from '@elastic/numeral';
import {
+ DATA_UNDEFINED_LABEL,
FCP_LABEL,
LONGEST_LONG_TASK,
NO_OF_LONG_TASK,
@@ -36,6 +37,11 @@ interface Props {
loading: boolean;
}
+function formatTitle(unit: string, value?: number) {
+ if (typeof value === 'undefined') return DATA_UNDEFINED_LABEL;
+ return formatToSec(value, unit);
+}
+
export function KeyUXMetrics({ data, loading }: Props) {
const uxQuery = useUxQuery();
@@ -62,7 +68,7 @@ export function KeyUXMetrics({ data, loading }: Props) {
@@ -70,7 +76,7 @@ export function KeyUXMetrics({ data, loading }: Props) {
@@ -78,7 +84,11 @@ export function KeyUXMetrics({ data, loading }: Props) {
@@ -86,7 +96,7 @@ export function KeyUXMetrics({ data, loading }: Props) {
@@ -94,7 +104,7 @@ export function KeyUXMetrics({ data, loading }: Props) {
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts
index e6d8f881bee577..8f3a71f669ecf2 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts
@@ -6,6 +6,13 @@
import { i18n } from '@kbn/i18n';
+export const DATA_UNDEFINED_LABEL = i18n.translate(
+ 'xpack.apm.rum.coreVitals.dataUndefined',
+ {
+ defaultMessage: 'N/A',
+ }
+);
+
export const FCP_LABEL = i18n.translate('xpack.apm.rum.coreVitals.fcp', {
defaultMessage: 'First contentful paint',
});
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx
index 75a018afa13d7e..18cd7d79cc69f2 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx
@@ -6,19 +6,14 @@
import React, { useCallback, useEffect } from 'react';
-import { EuiSuperSelect } from '@elastic/eui';
+import { EuiSelect } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
-import styled from 'styled-components';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { I18LABELS } from '../translations';
const DEFAULT_P = 50;
-const StyledSpan = styled.span`
- font-weight: 600;
-`;
-
export function UserPercentile() {
const history = useHistory();
@@ -49,32 +44,29 @@ export function UserPercentile() {
const options = [
{
value: '50',
- inputDisplay: I18LABELS.percentile50thMedian,
+ text: I18LABELS.percentile50thMedian,
dropdownDisplay: I18LABELS.percentile50thMedian,
'data-test-subj': 'p50Percentile',
},
{
value: '75',
- inputDisplay: {I18LABELS.percentile75th} ,
+ text: I18LABELS.percentile75th,
dropdownDisplay: I18LABELS.percentile75th,
'data-test-subj': 'p75Percentile',
},
{
value: '90',
- inputDisplay: {I18LABELS.percentile90th} ,
- dropdownDisplay: I18LABELS.percentile90th,
+ text: I18LABELS.percentile90th,
'data-test-subj': 'p90Percentile',
},
{
value: '95',
- inputDisplay: {I18LABELS.percentile95th} ,
- dropdownDisplay: I18LABELS.percentile95th,
+ text: I18LABELS.percentile95th,
'data-test-subj': 'p95Percentile',
},
{
value: '99',
- inputDisplay: {I18LABELS.percentile99th} ,
- dropdownDisplay: I18LABELS.percentile99th,
+ text: I18LABELS.percentile99th,
'data-test-subj': 'p99Percentile',
},
];
@@ -84,13 +76,12 @@ export function UserPercentile() {
};
return (
- onChange(value)}
+ onChange={(evt) => onChange(evt.target.value)}
/>
);
}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx
index 1198c014f59219..77afe92a8f5210 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx
@@ -20,7 +20,7 @@ import {
ViewMode,
isErrorEmbeddable,
} from '../../../../../../../../src/plugins/embeddable/public';
-import { getLayerList } from './LayerList';
+import { useLayerList } from './useLayerList';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { RenderTooltipContentParams } from '../../../../../../maps/public';
import { MapToolTip } from './MapToolTip';
@@ -55,6 +55,8 @@ export function EmbeddedMapComponent() {
const mapFilters = useMapFilters();
+ const layerList = useLayerList();
+
const [embeddable, setEmbeddable] = useState<
MapEmbeddable | ErrorEmbeddable | undefined
>();
@@ -148,7 +150,7 @@ export function EmbeddedMapComponent() {
if (embeddableObject && !isErrorEmbeddable(embeddableObject)) {
embeddableObject.setRenderTooltipContent(renderTooltipContent);
- await embeddableObject.setLayerList(getLayerList());
+ await embeddableObject.setLayerList(layerList);
}
setEmbeddable(embeddableObject);
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx
index 07b40addedec39..467e31f15a8de4 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx
@@ -18,7 +18,7 @@ import {
REGION_NAME,
TRANSACTION_DURATION_COUNTRY,
TRANSACTION_DURATION_REGION,
-} from './LayerList';
+} from './useLayerList';
import { RenderTooltipContentParams } from '../../../../../../maps/public';
import { I18LABELS } from '../translations';
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx
index 023f5d61a964e0..1053dd611d519a 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx
@@ -8,7 +8,7 @@ import { storiesOf } from '@storybook/react';
import React from 'react';
import { EuiThemeProvider } from '../../../../../../../observability/public';
import { MapToolTip } from '../MapToolTip';
-import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../LayerList';
+import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../useLayerList';
storiesOf('app/RumDashboard/VisitorsRegionMap', module)
.addDecorator((storyFn) => {storyFn()} )
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts
index c45f8b27d7d3e8..e5643325833759 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts
@@ -25,6 +25,11 @@ export const mockLayerList = [
id: '3657625d-17b0-41ef-99ba-3a2b2938655c',
indexPatternTitle: 'apm-*',
term: 'client.geo.country_iso_code',
+ whereQuery: {
+ language: 'kuery',
+ query:
+ 'transaction.type : "page-load" and service.name : "undefined"',
+ },
metrics: [
{
type: 'avg',
@@ -95,6 +100,11 @@ export const mockLayerList = [
id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41',
indexPatternTitle: 'apm-*',
term: 'client.geo.region_iso_code',
+ whereQuery: {
+ language: 'kuery',
+ query:
+ 'transaction.type : "page-load" and service.name : "undefined"',
+ },
metrics: [{ type: 'avg', field: 'transaction.duration.us' }],
indexPatternId: 'apm_static_index_pattern_id',
},
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts
similarity index 51%
rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts
rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts
index eb149ee2a132d0..872553452b2637 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts
@@ -4,14 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { renderHook } from '@testing-library/react-hooks';
import { mockLayerList } from './__mocks__/regions_layer.mock';
-import { getLayerList } from '../LayerList';
+import { useLayerList } from '../useLayerList';
-describe('LayerList', () => {
- describe('getLayerList', () => {
- test('it returns the region layer', () => {
- const layerList = getLayerList();
- expect(layerList).toStrictEqual(mockLayerList);
- });
+describe('useLayerList', () => {
+ test('it returns the region layer', () => {
+ const { result } = renderHook(() => useLayerList());
+ expect(result.current).toStrictEqual(mockLayerList);
});
});
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts
similarity index 79%
rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts
rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts
index 138a3f4018c651..bc45d58329f496 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts
@@ -22,8 +22,14 @@ import {
} from '../../../../../../maps/common/constants';
import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public';
+import { useUrlParams } from '../../../../hooks/useUrlParams';
+import {
+ SERVICE_NAME,
+ TRANSACTION_TYPE,
+} from '../../../../../common/elasticsearch_fieldnames';
+import { TRANSACTION_PAGE_LOAD } from '../../../../../common/transaction_types';
-const ES_TERM_SOURCE: ESTermSourceDescriptor = {
+const ES_TERM_SOURCE_COUNTRY: ESTermSourceDescriptor = {
type: 'ES_TERM_SOURCE',
id: '3657625d-17b0-41ef-99ba-3a2b2938655c',
indexPatternTitle: 'apm-*',
@@ -39,6 +45,26 @@ const ES_TERM_SOURCE: ESTermSourceDescriptor = {
applyGlobalQuery: true,
};
+const ES_TERM_SOURCE_REGION: ESTermSourceDescriptor = {
+ type: 'ES_TERM_SOURCE',
+ id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41',
+ indexPatternTitle: 'apm-*',
+ term: 'client.geo.region_iso_code',
+ metrics: [{ type: AGG_TYPE.AVG, field: 'transaction.duration.us' }],
+ whereQuery: {
+ query: 'transaction.type : "page-load"',
+ language: 'kuery',
+ },
+ indexPatternId: APM_STATIC_INDEX_PATTERN_ID,
+};
+
+const getWhereQuery = (serviceName: string) => {
+ return {
+ query: `${TRANSACTION_TYPE} : "${TRANSACTION_PAGE_LOAD}" and ${SERVICE_NAME} : "${serviceName}"`,
+ language: 'kuery',
+ };
+};
+
export const REGION_NAME = 'region_name';
export const COUNTRY_NAME = 'name';
@@ -56,7 +82,11 @@ interface VectorLayerDescriptor extends BaseVectorLayerDescriptor {
sourceDescriptor: EMSFileSourceDescriptor;
}
-export function getLayerList() {
+export function useLayerList() {
+ const { urlParams } = useUrlParams();
+
+ const { serviceName } = urlParams;
+
const baseLayer: LayerDescriptor = {
sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true },
id: 'b7af286d-2580-4f47-be93-9653d594ce7e',
@@ -69,6 +99,8 @@ export function getLayerList() {
type: 'VECTOR_TILE',
};
+ ES_TERM_SOURCE_COUNTRY.whereQuery = getWhereQuery(serviceName!);
+
const getLayerStyle = (fieldName: string): VectorStyleDescriptor => {
return {
type: 'VECTOR',
@@ -119,7 +151,7 @@ export function getLayerList() {
joins: [
{
leftField: 'iso2',
- right: ES_TERM_SOURCE,
+ right: ES_TERM_SOURCE_COUNTRY,
},
],
sourceDescriptor: {
@@ -138,18 +170,13 @@ export function getLayerList() {
type: 'VECTOR',
};
+ ES_TERM_SOURCE_REGION.whereQuery = getWhereQuery(serviceName!);
+
const pageLoadDurationByAdminRegionLayer: VectorLayerDescriptor = {
joins: [
{
leftField: 'region_iso_code',
- right: {
- type: 'ES_TERM_SOURCE',
- id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41',
- indexPatternTitle: 'apm-*',
- term: 'client.geo.region_iso_code',
- metrics: [{ type: AGG_TYPE.AVG, field: 'transaction.duration.us' }],
- indexPatternId: APM_STATIC_INDEX_PATTERN_ID,
- },
+ right: ES_TERM_SOURCE_REGION,
},
],
sourceDescriptor: {
@@ -166,6 +193,7 @@ export function getLayerList() {
visible: true,
type: 'VECTOR',
};
+
return [
baseLayer,
pageLoadDurationByCountryLayer,
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts
index c8db011874a894..a8c4d67305c989 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts
@@ -7,6 +7,9 @@
import { i18n } from '@kbn/i18n';
export const I18LABELS = {
+ dataMissing: i18n.translate('xpack.apm.rum.dashboard.dataMissing', {
+ defaultMessage: 'N/A',
+ }),
backEnd: i18n.translate('xpack.apm.rum.dashboard.backend', {
defaultMessage: 'Backend',
}),
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
index 01336a0e8f0ce4..7e18132e59cf31 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
@@ -22,7 +22,7 @@ describe('MLJobLink', () => {
);
expect(href).toMatchInlineSnapshot(
- `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:(),zoom:(from:now%2Fw,to:now-4h))"`
+ `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:())"`
);
});
it('should produce the correct URL with jobId, serviceName, and transactionType', async () => {
@@ -41,7 +41,7 @@ describe('MLJobLink', () => {
);
expect(href).toMatchInlineSnapshot(
- `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request)),zoom:(from:now%2Fw,to:now-4h))"`
+ `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request)))"`
);
});
@@ -61,7 +61,7 @@ describe('MLJobLink', () => {
);
expect(href).toMatchInlineSnapshot(
- `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:10000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-java,transaction.type:request)),zoom:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))"`
+ `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:10000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-java,transaction.type:request)))"`
);
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts
index a758f266b44171..0f671fd363c75a 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts
@@ -38,7 +38,6 @@ export function useTimeSeriesExplorerHref({
refreshPaused !== undefined && refreshInterval !== undefined
? { pause: refreshPaused, value: refreshInterval }
: undefined,
- zoom: timeRange,
...(serviceName && transactionType
? {
entities: {
diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx
index e64357c0852095..8aec4184f924d6 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx
@@ -17,7 +17,7 @@ import { callApmApi } from '../../../../services/rest/createCallApmApi';
// @ts-expect-error
import CustomPlot from '../CustomPlot';
-const tickFormatY = (y?: number) => {
+const tickFormatY = (y?: number | null) => {
return asPercent(y || 0, 1);
};
@@ -56,7 +56,7 @@ export function ErroneousTransactionsRateChart() {
[syncedChartsProps]
);
- const errorRates = data?.erroneousTransactionsRate || [];
+ const errorRates = data?.transactionErrorRate || [];
const maxRate = max(errorRates.map((errorRate) => errorRate.y));
return (
diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts
index d597b65040ce60..9eba18d44ad502 100644
--- a/x-pack/plugins/apm/server/feature.ts
+++ b/x-pack/plugins/apm/server/feature.ts
@@ -20,8 +20,6 @@ export const APM_FEATURE = {
}),
order: 900,
category: DEFAULT_APP_CATEGORIES.observability,
- icon: 'apmApp',
- navLinkId: 'apm',
app: ['apm', 'ux', 'kibana'],
catalogue: ['apm'],
management: {
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts
index c93fdfc15fe3ce..62fc16fb25053f 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts
@@ -49,7 +49,6 @@ export async function createApmTelemetry({
taskManager.registerTaskDefinitions({
[APM_TELEMETRY_TASK_NAME]: {
title: 'Collect APM usage',
- type: APM_TELEMETRY_TASK_NAME,
createTaskRunner: () => {
return {
run: async () => {
diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts
new file mode 100644
index 00000000000000..ccc7ba7e22b504
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EVENT_OUTCOME } from '../../../common/elasticsearch_fieldnames';
+import { EventOutcome } from '../../../common/event_outcome';
+import {
+ AggregationOptionsByType,
+ AggregationResultOf,
+} from '../../../typings/elasticsearch/aggregations';
+import { getTransactionDurationFieldForAggregatedTransactions } from './aggregated_transactions';
+
+export function getOutcomeAggregation({
+ searchAggregatedTransactions,
+}: {
+ searchAggregatedTransactions: boolean;
+}) {
+ return {
+ terms: { field: EVENT_OUTCOME },
+ aggs: {
+ count: {
+ value_count: {
+ field: getTransactionDurationFieldForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ },
+ },
+ },
+ };
+}
+
+export function calculateTransactionErrorPercentage(
+ outcomeResponse: AggregationResultOf<
+ ReturnType,
+ {}
+ >
+) {
+ const outcomes = Object.fromEntries(
+ outcomeResponse.buckets.map(({ key, count }) => [key, count.value])
+ );
+
+ const failedTransactions = outcomes[EventOutcome.failure] ?? 0;
+ const successfulTransactions = outcomes[EventOutcome.success] ?? 0;
+
+ return failedTransactions / (successfulTransactions + failedTransactions);
+}
+
+export function getTransactionErrorRateTimeSeries(
+ buckets: AggregationResultOf<
+ {
+ date_histogram: AggregationOptionsByType['date_histogram'];
+ aggs: { outcomes: ReturnType };
+ },
+ {}
+ >['buckets']
+) {
+ return buckets.map((dateBucket) => {
+ return {
+ x: dateBucket.key,
+ y: calculateTransactionErrorPercentage(dateBucket.outcomes),
+ };
+ });
+}
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts
index eb2ddbf38b2746..f7ca40ef1052c8 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts
@@ -44,7 +44,7 @@ describe('getServiceMapServiceNodeInfo', () => {
it('returns data', async () => {
jest.spyOn(getErrorRateModule, 'getErrorRate').mockResolvedValueOnce({
average: 0.5,
- erroneousTransactionsRate: [],
+ transactionErrorRate: [],
noHits: false,
});
diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts
index 65bc3f7e47171e..7d190c5b664501 100644
--- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts
@@ -28,7 +28,11 @@ import {
getMLJobIds,
getServiceAnomalies,
} from '../../service_map/get_service_anomalies';
-import { AggregationResultOf } from '../../../../typings/elasticsearch/aggregations';
+import {
+ calculateTransactionErrorPercentage,
+ getOutcomeAggregation,
+ getTransactionErrorRateTimeSeries,
+} from '../../helpers/transaction_error_rate';
function getDateHistogramOpts(start: number, end: number) {
return {
@@ -261,20 +265,7 @@ export const getTransactionErrorRates = async ({
}: AggregationParams) => {
const { apmEventClient, start, end } = setup;
- const outcomes = {
- terms: {
- field: EVENT_OUTCOME,
- },
- aggs: {
- count: {
- value_count: {
- field: getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- },
- },
- },
- };
+ const outcomes = getOutcomeAggregation({ searchAggregatedTransactions });
const response = await apmEventClient.search(
mergeProjection(projection, {
@@ -326,21 +317,6 @@ export const getTransactionErrorRates = async ({
return [];
}
- function calculateTransactionErrorPercentage(
- outcomeResponse: AggregationResultOf
- ) {
- const successfulTransactions =
- outcomeResponse.buckets.find(
- (bucket) => bucket.key === EventOutcome.success
- )?.count.value ?? 0;
- const failedTransactions =
- outcomeResponse.buckets.find(
- (bucket) => bucket.key === EventOutcome.failure
- )?.count.value ?? 0;
-
- return failedTransactions / (successfulTransactions + failedTransactions);
- }
-
return aggregations.services.buckets.map((serviceBucket) => {
const transactionErrorRate = calculateTransactionErrorPercentage(
serviceBucket.outcomes
@@ -349,12 +325,9 @@ export const getTransactionErrorRates = async ({
serviceName: serviceBucket.key as string,
transactionErrorRate: {
value: transactionErrorRate,
- timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => {
- return {
- x: dateBucket.key,
- y: calculateTransactionErrorPercentage(dateBucket.outcomes),
- };
- }),
+ timeseries: getTransactionErrorRateTimeSeries(
+ serviceBucket.timeseries.buckets
+ ),
},
};
});
diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts
index d5289430b2698d..e9d273dad6262b 100644
--- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts
+++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts
@@ -3,21 +3,25 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { mean } from 'lodash';
-import { EventOutcome } from '../../../common/event_outcome';
+
+import { Coordinate } from '../../../typings/timeseries';
+
import {
+ EVENT_OUTCOME,
+ SERVICE_NAME,
TRANSACTION_NAME,
TRANSACTION_TYPE,
- SERVICE_NAME,
- EVENT_OUTCOME,
} from '../../../common/elasticsearch_fieldnames';
+import { EventOutcome } from '../../../common/event_outcome';
import { rangeFilter } from '../../../common/utils/range_filter';
-import { Setup, SetupTimeRange } from '../helpers/setup_request';
+import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions';
import { getBucketSize } from '../helpers/get_bucket_size';
+import { Setup, SetupTimeRange } from '../helpers/setup_request';
import {
- getProcessorEventForAggregatedTransactions,
- getTransactionDurationFieldForAggregatedTransactions,
-} from '../helpers/aggregated_transactions';
+ calculateTransactionErrorPercentage,
+ getOutcomeAggregation,
+ getTransactionErrorRateTimeSeries,
+} from '../helpers/transaction_error_rate';
export async function getErrorRate({
serviceName,
@@ -31,7 +35,11 @@ export async function getErrorRate({
transactionName?: string;
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
-}) {
+}): Promise<{
+ noHits: boolean;
+ transactionErrorRate: Coordinate[];
+ average: number | null;
+}> {
const { start, end, esFilter, apmEventClient } = setup;
const transactionNamefilter = transactionName
@@ -52,6 +60,8 @@ export async function getErrorRate({
...esFilter,
];
+ const outcomes = getOutcomeAggregation({ searchAggregatedTransactions });
+
const params = {
apm: {
events: [
@@ -64,7 +74,8 @@ export async function getErrorRate({
size: 0,
query: { bool: { filter } },
aggs: {
- total_transactions: {
+ outcomes,
+ timeseries: {
date_histogram: {
field: '@timestamp',
fixed_interval: getBucketSize(start, end).intervalString,
@@ -72,20 +83,7 @@ export async function getErrorRate({
extended_bounds: { min: start, max: end },
},
aggs: {
- [EVENT_OUTCOME]: {
- terms: {
- field: EVENT_OUTCOME,
- },
- aggs: {
- count: {
- value_count: {
- field: getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- },
- },
- },
- },
+ outcomes,
},
},
},
@@ -96,31 +94,17 @@ export async function getErrorRate({
const noHits = resp.hits.total.value === 0;
- const erroneousTransactionsRate =
- resp.aggregations?.total_transactions.buckets.map((bucket) => {
- const successful =
- bucket[EVENT_OUTCOME].buckets.find(
- (eventOutcomeBucket) =>
- eventOutcomeBucket.key === EventOutcome.success
- )?.count.value ?? 0;
-
- const failed =
- bucket[EVENT_OUTCOME].buckets.find(
- (eventOutcomeBucket) =>
- eventOutcomeBucket.key === EventOutcome.failure
- )?.count.value ?? 0;
+ if (!resp.aggregations) {
+ return { noHits, transactionErrorRate: [], average: null };
+ }
- return {
- x: bucket.key,
- y: failed / (successful + failed),
- };
- }) || [];
+ const transactionErrorRate = getTransactionErrorRateTimeSeries(
+ resp.aggregations.timeseries.buckets
+ );
- const average = mean(
- erroneousTransactionsRate
- .map((errorRate) => errorRate.y)
- .filter((y) => isFinite(y))
+ const average = calculateTransactionErrorPercentage(
+ resp.aggregations.outcomes
);
- return { noHits, erroneousTransactionsRate, average };
+ return { noHits, transactionErrorRate, average };
}
diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts
index ac5392c9d3dee2..02499eae7eb670 100644
--- a/x-pack/plugins/canvas/server/plugin.ts
+++ b/x-pack/plugins/canvas/server/plugin.ts
@@ -43,8 +43,6 @@ export class CanvasPlugin implements Plugin {
name: 'Canvas',
order: 300,
category: DEFAULT_APP_CATEGORIES.kibana,
- icon: 'canvasApp',
- navLinkId: 'canvas',
app: ['canvas', 'kibana'],
catalogue: ['canvas'],
privileges: {
diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts
index 1c3a770da79f55..45005f3f5e4227 100644
--- a/x-pack/plugins/cloud/public/plugin.ts
+++ b/x-pack/plugins/cloud/public/plugin.ts
@@ -22,6 +22,7 @@ interface CloudSetupDependencies {
export interface CloudSetup {
cloudId?: string;
+ cloudDeploymentUrl?: string;
isCloudEnabled: boolean;
}
@@ -33,7 +34,7 @@ export class CloudPlugin implements Plugin {
}
public async setup(core: CoreSetup, { home }: CloudSetupDependencies) {
- const { id, resetPasswordUrl } = this.config;
+ const { id, resetPasswordUrl, deploymentUrl } = this.config;
const isCloudEnabled = getIsCloudEnabled(id);
if (home) {
@@ -45,6 +46,7 @@ export class CloudPlugin implements Plugin {
return {
cloudId: id,
+ cloudDeploymentUrl: deploymentUrl,
isCloudEnabled,
};
}
diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts
index 5b634fe4cf26c1..3360248ebb34e2 100644
--- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts
@@ -98,6 +98,18 @@ describe('EQL search strategy', () => {
expect(mockEqlSearch).not.toHaveBeenCalled();
expect(requestParams).toEqual(expect.objectContaining({ id: 'my-search-id' }));
});
+
+ it('emits an error if the client throws', async () => {
+ expect.assertions(1);
+ mockEqlSearch.mockReset().mockRejectedValueOnce(new Error('client error'));
+ const eqlSearch = await eqlSearchStrategyProvider(mockLogger);
+ eqlSearch.search({ options, params }, {}, mockContext).subscribe(
+ () => {},
+ (err) => {
+ expect(err).toEqual(new Error('client error'));
+ }
+ );
+ });
});
describe('arguments', () => {
diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts
index a7ca999699e237..f8dd1dca740b7e 100644
--- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts
@@ -29,48 +29,53 @@ export const eqlSearchStrategyProvider = (
},
search: (request, options, context) =>
from(
- new Promise(async (resolve) => {
+ new Promise(async (resolve, reject) => {
logger.debug(`_eql/search ${JSON.stringify(request.params) || request.id}`);
let promise: TransportRequestPromise;
- const eqlClient = context.core.elasticsearch.client.asCurrentUser.eql;
- const uiSettingsClient = await context.core.uiSettings.client;
- const asyncOptions = getAsyncOptions();
- const searchOptions = toSnakeCase({ ...request.options });
- if (request.id) {
- promise = eqlClient.get(
- {
- id: request.id,
- ...toSnakeCase(asyncOptions),
- },
- searchOptions
- );
- } else {
- const { ignoreThrottled, ignoreUnavailable } = await getDefaultSearchParams(
- uiSettingsClient
- );
- const searchParams = toSnakeCase({
- ignoreThrottled,
- ignoreUnavailable,
- ...asyncOptions,
- ...request.params,
- });
+ try {
+ const eqlClient = context.core.elasticsearch.client.asCurrentUser.eql;
+ const uiSettingsClient = await context.core.uiSettings.client;
+ const asyncOptions = getAsyncOptions();
+ const searchOptions = toSnakeCase({ ...request.options });
- promise = eqlClient.search(
- searchParams as EqlSearchStrategyRequest['params'],
- searchOptions
- );
- }
+ if (request.id) {
+ promise = eqlClient.get(
+ {
+ id: request.id,
+ ...toSnakeCase(asyncOptions),
+ },
+ searchOptions
+ );
+ } else {
+ const { ignoreThrottled, ignoreUnavailable } = await getDefaultSearchParams(
+ uiSettingsClient
+ );
+ const searchParams = toSnakeCase({
+ ignoreThrottled,
+ ignoreUnavailable,
+ ...asyncOptions,
+ ...request.params,
+ });
+
+ promise = eqlClient.search(
+ searchParams as EqlSearchStrategyRequest['params'],
+ searchOptions
+ );
+ }
- const rawResponse = await shimAbortSignal(promise, options?.abortSignal);
- const { id, is_partial: isPartial, is_running: isRunning } = rawResponse.body;
+ const rawResponse = await shimAbortSignal(promise, options?.abortSignal);
+ const { id, is_partial: isPartial, is_running: isRunning } = rawResponse.body;
- resolve({
- id,
- isPartial,
- isRunning,
- rawResponse,
- });
+ resolve({
+ id,
+ isPartial,
+ isRunning,
+ rawResponse,
+ });
+ } catch (e) {
+ reject(e);
+ }
})
),
};
diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts
index 43b0be8a5b438f..4b1040de52f66b 100644
--- a/x-pack/plugins/enterprise_search/server/plugin.ts
+++ b/x-pack/plugins/enterprise_search/server/plugin.ts
@@ -85,7 +85,6 @@ export class EnterpriseSearchPlugin implements Plugin {
name: ENTERPRISE_SEARCH_PLUGIN.NAME,
order: 0,
category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
- icon: 'logoEnterpriseSearch',
app: [
'kibana',
ENTERPRISE_SEARCH_PLUGIN.ID,
diff --git a/x-pack/plugins/features/common/kibana_feature.ts b/x-pack/plugins/features/common/kibana_feature.ts
index 32a75029567288..04b40843eb6d0f 100644
--- a/x-pack/plugins/features/common/kibana_feature.ts
+++ b/x-pack/plugins/features/common/kibana_feature.ts
@@ -6,6 +6,7 @@
import { RecursiveReadonly } from '@kbn/utility-types';
import { AppCategory } from 'src/core/types';
+import { LicenseType } from '../../licensing/common/types';
import { FeatureKibanaPrivileges } from './feature_kibana_privileges';
import { SubFeatureConfig, SubFeature as KibanaSubFeature } from './sub_feature';
import { ReservedKibanaPrivilege } from './reserved_kibana_privilege';
@@ -52,25 +53,12 @@ export interface KibanaFeatureConfig {
excludeFromBasePrivileges?: boolean;
/**
- * Optional array of supported licenses.
+ * Optional minimum supported license.
* If omitted, all licenses are allowed.
* This does not restrict access to your feature based on license.
* Its only purpose is to inform the space and roles UIs on which features to display.
*/
- validLicenses?: ReadonlyArray<
- 'basic' | 'standard' | 'gold' | 'platinum' | 'enterprise' | 'trial'
- >;
-
- /**
- * An optional EUI Icon to be used when displaying your feature.
- */
- icon?: string;
-
- /**
- * The optional Nav Link ID for feature.
- * If specified, your link will be automatically hidden if needed based on the current space and user permissions.
- */
- navLinkId?: string;
+ minimumLicense?: LicenseType;
/**
* An array of app ids that are enabled when this feature is enabled.
@@ -170,10 +158,6 @@ export class KibanaFeature {
return this.config.category;
}
- public get navLinkId() {
- return this.config.navLinkId;
- }
-
public get app() {
return this.config.app;
}
@@ -186,12 +170,8 @@ export class KibanaFeature {
return this.config.management;
}
- public get icon() {
- return this.config.icon;
- }
-
- public get validLicenses() {
- return this.config.validLicenses;
+ public get minimumLicense() {
+ return this.config.minimumLicense;
}
public get privileges() {
diff --git a/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap
index fdeb53dd2fa125..3043de27534f1c 100644
--- a/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap
+++ b/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap
@@ -12,12 +12,6 @@ exports[`FeatureRegistry Kibana Features prevents features from being registered
exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains_invalid()_chars" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`;
-exports[`FeatureRegistry Kibana Features prevents features from being registered with a navLinkId of "" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" is not allowed to be empty]"`;
-
-exports[`FeatureRegistry Kibana Features prevents features from being registered with a navLinkId of "contains space" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]"`;
-
-exports[`FeatureRegistry Kibana Features prevents features from being registered with a navLinkId of "contains_invalid()_chars" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]"`;
-
exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "catalogue" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`;
exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "doesn't match valid regex" 1`] = `"child \\"id\\" fails because [\\"id\\" with value \\"doesn't match valid regex\\" fails to match the required pattern: /^[a-zA-Z0-9_-]+$/]"`;
diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts
index aaaeccbd15e724..fda72e45369397 100644
--- a/x-pack/plugins/features/server/feature_registry.test.ts
+++ b/x-pack/plugins/features/server/feature_registry.test.ts
@@ -33,11 +33,9 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
excludeFromBasePrivileges: true,
- icon: 'addDataApp',
- navLinkId: 'someNavLink',
app: ['app1'],
category: { id: 'foo', label: 'foo' },
- validLicenses: ['standard', 'basic', 'gold', 'platinum'],
+ minimumLicense: 'platinum',
catalogue: ['foo'],
management: {
foo: ['bar'],
@@ -421,20 +419,6 @@ describe('FeatureRegistry', () => {
});
['contains space', 'contains_invalid()_chars', ''].forEach((prohibitedChars) => {
- it(`prevents features from being registered with a navLinkId of "${prohibitedChars}"`, () => {
- const featureRegistry = new FeatureRegistry();
- expect(() =>
- featureRegistry.registerKibanaFeature({
- id: 'foo',
- name: 'some feature',
- navLinkId: prohibitedChars,
- app: [],
- category: { id: 'foo', label: 'foo' },
- privileges: null,
- })
- ).toThrowErrorMatchingSnapshot();
- });
-
it(`prevents features from being registered with a management id of "${prohibitedChars}"`, () => {
const featureRegistry = new FeatureRegistry();
expect(() =>
diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts
index c6ec2d52c6d1ae..78ffcdb0873604 100644
--- a/x-pack/plugins/features/server/feature_schema.ts
+++ b/x-pack/plugins/features/server/feature_schema.ts
@@ -91,12 +91,14 @@ const kibanaFeatureSchema = Joi.object({
category: appCategorySchema,
order: Joi.number(),
excludeFromBasePrivileges: Joi.boolean(),
- validLicenses: Joi.array().items(
- Joi.string().valid('basic', 'standard', 'gold', 'platinum', 'enterprise', 'trial')
+ minimumLicense: Joi.string().valid(
+ 'basic',
+ 'standard',
+ 'gold',
+ 'platinum',
+ 'enterprise',
+ 'trial'
),
- icon: Joi.string(),
- description: Joi.string(),
- navLinkId: Joi.string().regex(uiCapabilitiesRegex),
app: Joi.array().items(Joi.string()).required(),
management: managementSchema,
catalogue: catalogueSchema,
diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts
index 4cec44d6fa19a3..d420aea49c6e13 100644
--- a/x-pack/plugins/features/server/oss_features.ts
+++ b/x-pack/plugins/features/server/oss_features.ts
@@ -21,8 +21,6 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
}),
order: 100,
category: DEFAULT_APP_CATEGORIES.kibana,
- icon: 'discoverApp',
- navLinkId: 'discover',
app: ['discover', 'kibana'],
catalogue: ['discover'],
privileges: {
@@ -82,8 +80,6 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
}),
order: 700,
category: DEFAULT_APP_CATEGORIES.kibana,
- icon: 'visualizeApp',
- navLinkId: 'visualize',
app: ['visualize', 'lens', 'kibana'],
catalogue: ['visualize'],
privileges: {
@@ -143,8 +139,6 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
}),
order: 200,
category: DEFAULT_APP_CATEGORIES.kibana,
- icon: 'dashboardApp',
- navLinkId: 'dashboards',
app: ['dashboards', 'kibana'],
catalogue: ['dashboard'],
privileges: {
@@ -222,8 +216,6 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
}),
order: 1300,
category: DEFAULT_APP_CATEGORIES.management,
- icon: 'devToolsApp',
- navLinkId: 'dev_tools',
app: ['dev_tools', 'kibana'],
catalogue: ['console', 'searchprofiler', 'grokdebugger'],
privileges: {
@@ -260,7 +252,6 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
}),
order: 1500,
category: DEFAULT_APP_CATEGORIES.management,
- icon: 'advancedSettingsApp',
app: ['kibana'],
catalogue: ['advanced_settings'],
management: {
@@ -300,7 +291,6 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
}),
order: 1600,
category: DEFAULT_APP_CATEGORIES.management,
- icon: 'indexPatternApp',
app: ['kibana'],
catalogue: ['indexPatterns'],
management: {
@@ -340,7 +330,6 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
}),
order: 1700,
category: DEFAULT_APP_CATEGORIES.management,
- icon: 'savedObjectsApp',
app: ['kibana'],
catalogue: ['saved_objects'],
management: {
@@ -384,8 +373,6 @@ const timelionFeature: KibanaFeatureConfig = {
name: 'Timelion',
order: 350,
category: DEFAULT_APP_CATEGORIES.kibana,
- icon: 'timelionApp',
- navLinkId: 'timelion',
app: ['timelion', 'kibana'],
catalogue: ['timelion'],
privileges: {
diff --git a/x-pack/plugins/features/server/routes/index.test.ts b/x-pack/plugins/features/server/routes/index.test.ts
index 692a8892031317..7080f18906146d 100644
--- a/x-pack/plugins/features/server/routes/index.test.ts
+++ b/x-pack/plugins/features/server/routes/index.test.ts
@@ -55,7 +55,7 @@ describe('GET /api/features', () => {
name: 'Licensed Feature',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
- validLicenses: ['gold'],
+ minimumLicense: 'gold',
privileges: null,
});
diff --git a/x-pack/plugins/features/server/routes/index.ts b/x-pack/plugins/features/server/routes/index.ts
index b5a4203d7a7685..1b0cd207753526 100644
--- a/x-pack/plugins/features/server/routes/index.ts
+++ b/x-pack/plugins/features/server/routes/index.ts
@@ -33,10 +33,9 @@ export function defineRoutes({ router, featureRegistry }: RouteDefinitionParams)
.filter(
(feature) =>
request.query.ignoreValidLicenses ||
- !feature.validLicenses ||
- !feature.validLicenses.length ||
- (context.licensing!.license.type &&
- feature.validLicenses.includes(context.licensing!.license.type))
+ !feature.minimumLicense ||
+ (context.licensing!.license &&
+ context.licensing!.license.hasAtLeast(feature.minimumLicense))
)
.sort(
(f1, f2) =>
diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts
index f5ba17a632c926..ffb36840a58502 100644
--- a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts
+++ b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts
@@ -92,7 +92,6 @@ describe('populateUICapabilities', () => {
new KibanaFeature({
id: 'newFeature',
name: 'my new feature',
- navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
@@ -146,7 +145,6 @@ describe('populateUICapabilities', () => {
new KibanaFeature({
id: 'newFeature',
name: 'my new feature',
- navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
catalogue: ['anotherFooEntry', 'anotherBarEntry'],
@@ -216,7 +214,6 @@ describe('populateUICapabilities', () => {
new KibanaFeature({
id: 'newFeature',
name: 'my new feature',
- navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
@@ -247,7 +244,6 @@ describe('populateUICapabilities', () => {
new KibanaFeature({
id: 'newFeature',
name: 'my new feature',
- navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: null,
@@ -292,7 +288,6 @@ describe('populateUICapabilities', () => {
new KibanaFeature({
id: 'newFeature',
name: 'my new feature',
- navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
@@ -364,7 +359,6 @@ describe('populateUICapabilities', () => {
new KibanaFeature({
id: 'newFeature',
name: 'my new feature',
- navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
@@ -385,7 +379,6 @@ describe('populateUICapabilities', () => {
new KibanaFeature({
id: 'yetAnotherNewFeature',
name: 'yet another new feature',
- navLinkId: 'yetAnotherNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts
index 21c50bf82f4bcf..85f25542b2efbb 100644
--- a/x-pack/plugins/graph/server/plugin.ts
+++ b/x-pack/plugins/graph/server/plugin.ts
@@ -49,11 +49,9 @@ export class GraphPlugin implements Plugin {
}),
order: 600,
category: DEFAULT_APP_CATEGORIES.kibana,
- icon: 'graphApp',
- navLinkId: 'graph',
app: ['graph', 'kibana'],
catalogue: ['graph'],
- validLicenses: ['platinum', 'enterprise', 'trial'],
+ minimumLicense: 'platinum',
privileges: {
all: {
app: ['graph', 'kibana'],
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
index 9d31480fb42c7c..73116956097f16 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
@@ -809,7 +809,7 @@ describe('edit policy', () => {
httpRequestsMockHelpers.setPoliciesResponse(policies);
});
- describe('with legacy data role config', () => {
+ describe('with deprecated data role config', () => {
test('should hide data tier option on cloud using legacy node role configuration', async () => {
http.setupNodeListResponse({
nodesByAttributes: { test: ['123'] },
@@ -847,6 +847,8 @@ describe('edit policy', () => {
expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy();
expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy();
expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy();
+ // We should not be showing the call-to-action for users to activate data tiers in cloud
+ expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeFalsy();
});
test('should show cloud notice when cold tier nodes do not exist', async () => {
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx
index 2dff55ac10de18..fc87b553ba5214 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx
@@ -5,22 +5,50 @@
*/
import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
import React, { FunctionComponent } from 'react';
-import { EuiCallOut } from '@elastic/eui';
+import { EuiCallOut, EuiLink } from '@elastic/eui';
+
+import { useKibana } from '../../../../../shared_imports';
+
+const deployment = i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body.elasticDeploymentLink',
+ {
+ defaultMessage: 'deployment',
+ }
+);
const i18nTexts = {
- title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title', {
+ title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.coldTierTitle', {
defaultMessage: 'Create a cold tier',
}),
- body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body', {
- defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.',
- }),
+ body: (deploymentUrl?: string) => {
+ return (
+
+ {deployment}
+
+ ) : (
+ deployment
+ ),
+ }}
+ />
+ );
+ },
};
export const CloudDataTierCallout: FunctionComponent = () => {
+ const {
+ services: { cloud },
+ } = useKibana();
+
return (
- {i18nTexts.body}
+ {i18nTexts.body(cloud?.cloudDeploymentUrl)}
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx
index 3911925751ceb9..da6c358aa67c15 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx
@@ -7,6 +7,7 @@
import React, { FunctionComponent, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
+import { get } from 'lodash';
import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui';
@@ -56,10 +57,12 @@ export const ColdPhase: FunctionComponent = ({
errors,
isShowingErrors,
}) => {
- const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({
+ const [formData] = useFormData({
watch: [useRolloverPath],
});
+ const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
+
return (
<>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx
index 59e4738657be4a..78ae66327654c8 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx
@@ -5,6 +5,7 @@
*/
import React, { FunctionComponent, Fragment } from 'react';
+import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui';
@@ -48,10 +49,12 @@ export const DeletePhase: FunctionComponent
= ({
isShowingErrors,
getUrlForApp,
}) => {
- const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({
- watch: [useRolloverPath],
+ const [formData] = useFormData({
+ watch: useRolloverPath,
});
+ const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
+
return (
{
- const [{ [useRolloverPath]: isRolloverEnabled }] = useFormData({ watch: [useRolloverPath] });
const form = useFormContext();
+ const [formData] = useFormData({
+ watch: useRolloverPath,
+ });
+ const isRolloverEnabled = get(formData, useRolloverPath);
const isShowingErrors = form.isValid === false;
const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_legacy_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_legacy_field.tsx
index 6dffca9ed358c3..611c26d7926c3b 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_legacy_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_legacy_field.tsx
@@ -60,20 +60,19 @@ export const DataTierAllocationFieldLegacy: FunctionComponent = ({
switch (phaseData.dataTierAllocationType) {
case 'default':
const isCloudEnabled = cloud?.isCloudEnabled ?? false;
- const isUsingNodeRoles = !isUsingDeprecatedDataRoleConfig;
- if (
- isCloudEnabled &&
- isUsingNodeRoles &&
- phase === 'cold' &&
- !nodesByRoles.data_cold?.length
- ) {
- // Tell cloud users they can deploy cold tier nodes.
- return (
- <>
-
-
- >
- );
+ if (isCloudEnabled && phase === 'cold') {
+ const isUsingNodeRolesAllocation = !isUsingDeprecatedDataRoleConfig;
+ const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length;
+
+ if (isUsingNodeRolesAllocation && hasNoNodesWithNodeRole) {
+ // Tell cloud users they can deploy nodes on cloud.
+ return (
+ <>
+
+
+ >
+ );
+ }
}
const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
index d03b36fceefdd0..7b1a4f44b5de63 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
@@ -7,6 +7,7 @@
import React, { Fragment, FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
+import { get } from 'lodash';
import {
EuiTextColor,
@@ -50,16 +51,15 @@ const warmProperty: keyof Phases = 'warm';
export const WarmPhase: FunctionComponent = () => {
const { originalPolicy } = useEditPolicyContext();
const form = useFormContext();
- const [
- {
- [useRolloverPath]: hotPhaseRolloverEnabled,
- '_meta.warm.enabled': enabled,
- '_meta.warm.warmPhaseOnRollover': warmPhaseOnRollover,
- },
- ] = useFormData({
+ const [formData] = useFormData({
watch: [useRolloverPath, '_meta.warm.enabled', '_meta.warm.warmPhaseOnRollover'],
});
+
+ const enabled = get(formData, '_meta.warm.enabled');
+ const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
+ const warmPhaseOnRollover = get(formData, '_meta.warm.warmPhaseOnRollover');
const isShowingErrors = form.isValid === false;
+
return (
<>
diff --git a/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx
index f818f49d8aa592..8e8514d1b51653 100644
--- a/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx
+++ b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx
@@ -125,9 +125,9 @@ export const SimulateTemplateFlyoutContent = ({
-
+ >
{(formData) => {
- return ;
+ return ;
}}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/date_range_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/date_range_datatype.test.tsx
new file mode 100644
index 00000000000000..3e1a8ae8f698ee
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/date_range_datatype.test.tsx
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { act } from 'react-dom/test-utils';
+
+import { componentHelpers, MappingsEditorTestBed } from '../helpers';
+
+const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor;
+
+// Parameters automatically added to the date range datatype when saved (with the default values)
+export const defaultDateRangeParameters = {
+ type: 'date_range',
+ coerce: true,
+ index: true,
+ store: false,
+};
+
+describe('Mappings editor: date range datatype', () => {
+ /**
+ * Variable to store the mappings data forwarded to the consumer component
+ */
+ let data: any;
+ let onChangeHandler: jest.Mock = jest.fn();
+ let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler);
+ let testBed: MappingsEditorTestBed;
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ beforeEach(() => {
+ onChangeHandler = jest.fn();
+ getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler);
+ });
+
+ test('should require a scaling factor to be provided', async () => {
+ const defaultMappings = {
+ properties: {
+ myField: {
+ type: 'double_range',
+ },
+ },
+ };
+
+ const updatedMappings = { ...defaultMappings };
+
+ await act(async () => {
+ testBed = setup({ value: defaultMappings, onChange: onChangeHandler });
+ });
+ testBed.component.update();
+
+ const {
+ component,
+ find,
+ exists,
+ actions: { startEditField, updateFieldAndCloseFlyout, toggleFormRow },
+ } = testBed;
+
+ // Open the flyout to edit the field
+ await startEditField('myField');
+
+ expect(exists('formatParameter')).toBe(false);
+
+ // Change the type to "date_range"
+ await act(async () => {
+ find('mappingsEditorFieldEdit.fieldSubType').simulate('change', [
+ {
+ label: 'Date range',
+ value: 'date_range',
+ },
+ ]);
+ });
+ component.update();
+
+ expect(exists('formatParameter')).toBe(true);
+ expect(exists('formatParameter.formatInput')).toBe(false);
+ toggleFormRow('formatParameter');
+ expect(exists('formatParameter.formatInput')).toBe(true);
+
+ await act(async () => {
+ find('formatParameter.formatInput').simulate('change', [{ label: 'customDateFormat' }]);
+ });
+ component.update();
+
+ await updateFieldAndCloseFlyout();
+
+ // It should have the default parameters values added, plus the scaling factor
+ updatedMappings.properties.myField = {
+ ...defaultDateRangeParameters,
+ format: 'customDateFormat',
+ } as any;
+
+ ({ data } = await getMappingsEditorData(component));
+ expect(data).toEqual(updatedMappings);
+ });
+});
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx
new file mode 100644
index 00000000000000..117695a43e6b4e
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { act } from 'react-dom/test-utils';
+
+import { componentHelpers, MappingsEditorTestBed } from '../helpers';
+
+const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor;
+
+// Parameters automatically added to the scaled float datatype when saved (with the default values)
+export const defaultScaledFloatParameters = {
+ type: 'scaled_float',
+ coerce: true,
+ doc_values: true,
+ ignore_malformed: false,
+ index: true,
+ store: false,
+};
+
+describe('Mappings editor: scaled float datatype', () => {
+ /**
+ * Variable to store the mappings data forwarded to the consumer component
+ */
+ let data: any;
+ let onChangeHandler: jest.Mock = jest.fn();
+ let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler);
+ let testBed: MappingsEditorTestBed;
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ beforeEach(() => {
+ onChangeHandler = jest.fn();
+ getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler);
+ });
+
+ test('should require a scaling factor to be provided', async () => {
+ const defaultMappings = {
+ properties: {
+ myField: {
+ type: 'byte',
+ },
+ },
+ };
+
+ const updatedMappings = { ...defaultMappings };
+
+ await act(async () => {
+ testBed = setup({ value: defaultMappings, onChange: onChangeHandler });
+ });
+ testBed.component.update();
+
+ const {
+ component,
+ find,
+ exists,
+ form,
+ actions: { startEditField, updateFieldAndCloseFlyout },
+ } = testBed;
+
+ // Open the flyout to edit the field
+ await startEditField('myField');
+
+ // Change the type to "scaled_float"
+ await act(async () => {
+ find('mappingsEditorFieldEdit.fieldSubType').simulate('change', [
+ {
+ label: 'Scaled float',
+ value: 'scaled_float',
+ },
+ ]);
+ });
+ component.update();
+
+ // It should **not** allow to save as the "scaling factor" parameter has not been set
+ await updateFieldAndCloseFlyout();
+ expect(exists('mappingsEditorFieldEdit')).toBe(true);
+ expect(form.getErrorsMessages()).toEqual(['A scaling factor is required.']);
+
+ await act(async () => {
+ form.setInputValue('scalingFactor.input', '123');
+ });
+ await updateFieldAndCloseFlyout();
+ expect(exists('mappingsEditorFieldEdit')).toBe(false);
+
+ // It should have the default parameters values added, plus the scaling factor
+ updatedMappings.properties.myField = {
+ ...defaultScaledFloatParameters,
+ scaling_factor: 123,
+ } as any;
+
+ ({ data } = await getMappingsEditorData(component));
+ expect(data).toEqual(updatedMappings);
+ });
+});
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx
index 2eb56a97dc3a01..a625cc8c0ab4bb 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx
@@ -358,9 +358,11 @@ export type TestSubjects =
| 'toggleExpandButton'
| 'createFieldForm'
| 'createFieldForm.fieldType'
+ | 'createFieldForm.fieldSubType'
| 'createFieldForm.addButton'
| 'mappingsEditorFieldEdit'
| 'mappingsEditorFieldEdit.fieldType'
+ | 'mappingsEditorFieldEdit.fieldSubType'
| 'mappingsEditorFieldEdit.editFieldUpdateButton'
| 'mappingsEditorFieldEdit.flyoutTitle'
| 'mappingsEditorFieldEdit.documentationLink'
@@ -383,4 +385,7 @@ export type TestSubjects =
| 'searchQuoteAnalyzer-custom.input'
| 'useSameAnalyzerForSearchCheckBox.input'
| 'metaParameterEditor'
+ | 'scalingFactor.input'
+ | 'formatParameter'
+ | 'formatParameter.formatInput'
| string;
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx
index c5001740c26c66..cbb39b70f965f5 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx
@@ -54,9 +54,7 @@ export const DynamicMappingSection = () => (
{(formData) => {
const {
- 'dynamicMapping.enabled': enabled,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- 'dynamicMapping.date_detection': dateDetection,
+ dynamicMapping: { enabled, date_detection: dateDetection },
} = formData;
if (enabled === undefined) {
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx
index d195f1abfc4443..aafdc7e58898d4 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx
@@ -155,7 +155,9 @@ export const SourceFieldSection = () => {
>
{(formData) => {
- const { 'sourceField.enabled': enabled } = formData;
+ const {
+ sourceField: { enabled },
+ } = formData;
if (enabled === undefined) {
return null;
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx
index c966df82fb5072..eb5af5eed46a05 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx
@@ -53,7 +53,7 @@ export const AnalyzerParameterSelects = ({
useEffect(() => {
const subscription = subscribe((updateData) => {
- const formData = updateData.data.raw;
+ const formData = updateData.data.internal;
const value = formData.sub ? formData.sub : formData.main;
onChange(value);
});
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx
index c2fe37559ae517..69e7a0e88bb166 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx
@@ -55,6 +55,7 @@ export const FormatParameter = ({ defaultValue, defaultToggleValue }: Props) =>
href: documentationService.getFormatLink(),
}}
defaultToggleValue={defaultToggleValue}
+ data-test-subj="formatParameter"
>
{(formatField) => {
@@ -81,6 +82,7 @@ export const FormatParameter = ({ defaultValue, defaultToggleValue }: Props) =>
setComboBoxOptions([...comboBoxOptions, newOption]);
}}
fullWidth
+ data-test-subj="formatInput"
/>
);
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/subtype_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/subtype_parameter.tsx
index cfa8b60653d4c0..9ad0f25d2a4ec8 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/subtype_parameter.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/subtype_parameter.tsx
@@ -85,6 +85,7 @@ export const SubTypeParameter = ({
selectedOptions={subTypeField.value as ComboBoxOption[]}
onChange={subTypeField.setValue}
isClearable={false}
+ data-test-subj="fieldSubType"
/>
);
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx
index ce349b2c6104fc..0a03344a662ecc 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx
@@ -5,6 +5,7 @@
*/
import React, { useState } from 'react';
+import { get } from 'lodash';
import {
EuiFlexGroup,
EuiFlexItem,
@@ -193,7 +194,7 @@ export const EditFieldFormRow = React.memo(
return formFieldPath ? (
{(formData) => {
- setIsContentVisible(formData[formFieldPath]);
+ setIsContentVisible(get(formData, formFieldPath));
return renderContent();
}}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx
index 6ad3c9c5d0bd4c..2bdb6f10d65f37 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx
@@ -6,7 +6,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
-import { NormalizedField, Field as FieldType } from '../../../../types';
+import { NormalizedField, Field as FieldType, ComboBoxOption } from '../../../../types';
import { getFieldConfig } from '../../../../lib';
import { UseField, FormDataProvider, NumericField, Field } from '../../../../shared_imports';
import {
@@ -48,9 +48,9 @@ export const NumericType = ({ field }: Props) => {
<>
{/* scaling_factor */}
-
- {(formData) =>
- formData.subType === 'scaled_float' ? (
+ pathsToWatch="subType">
+ {(formData) => {
+ return formData.subType?.[0]?.value === 'scaled_float' ? (
{
path="scaling_factor"
config={getFieldConfig('scaling_factor')}
component={Field}
+ data-test-subj="scalingFactor"
/>
- ) : null
- }
+ ) : null;
+ }}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx
index 9a37f55ac8e9d2..b4bab6d35af34f 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx
@@ -5,7 +5,12 @@
*/
import React from 'react';
-import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types';
+import {
+ NormalizedField,
+ Field as FieldType,
+ ParameterName,
+ ComboBoxOption,
+} from '../../../../types';
import { getFieldConfig } from '../../../../lib';
import {
StoreParameter,
@@ -33,9 +38,9 @@ export const RangeType = ({ field }: Props) => {
-
+ pathsToWatch="subType">
{(formData) =>
- formData.subType === 'date_range' ? (
+ formData.subType?.[0]?.value === 'date_range' ? (
{
-
+ pathsToWatch="subType">
{(formData) =>
- formData.subType === 'date_range' ? (
+ formData.subType?.[0]?.value === 'date_range' ? (
) : null
}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx
index a402dec250056d..4912b0963bc121 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx
@@ -18,7 +18,7 @@ export const StateProvider: React.FC = ({ children }) => {
configuration: {
defaultValue: {},
data: {
- raw: {},
+ internal: {},
format: () => ({}),
},
validate: () => Promise.resolve(true),
@@ -26,7 +26,7 @@ export const StateProvider: React.FC = ({ children }) => {
templates: {
defaultValue: {},
data: {
- raw: {},
+ internal: {},
format: () => ({}),
},
validate: () => Promise.resolve(true),
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
index e7efd6f28343bc..47e9d5200ea084 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
@@ -176,7 +176,7 @@ export const reducer = (state: State, action: Action): State => {
configuration: {
...state.configuration,
data: {
- raw: action.value.configuration,
+ internal: action.value.configuration,
format: () => action.value.configuration,
},
defaultValue: action.value.configuration,
@@ -184,7 +184,7 @@ export const reducer = (state: State, action: Action): State => {
templates: {
...state.templates,
data: {
- raw: action.value.templates,
+ internal: action.value.templates,
format: () => action.value.templates,
},
defaultValue: action.value.templates,
@@ -217,7 +217,7 @@ export const reducer = (state: State, action: Action): State => {
isValid: true,
defaultValue: action.value,
data: {
- raw: action.value,
+ internal: action.value,
format: () => action.value,
},
validate: async () => true,
@@ -241,7 +241,7 @@ export const reducer = (state: State, action: Action): State => {
isValid: true,
defaultValue: action.value,
data: {
- raw: action.value,
+ internal: action.value,
format: () => action.value,
},
validate: async () => true,
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx
index feba79ce85e85f..8d039475f9cf8f 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx
@@ -35,8 +35,8 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => {
const isFieldFormVisible = state.fieldForm !== undefined;
const emptyNameValue =
isFieldFormVisible &&
- (state.fieldForm!.data.raw.name === undefined ||
- state.fieldForm!.data.raw.name.trim() === '');
+ (state.fieldForm!.data.internal.name === undefined ||
+ state.fieldForm!.data.internal.name.trim() === '');
const bypassFieldFormValidation =
state.documentFields.status === 'creatingField' && emptyNameValue;
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx
index 56f040fc59a7b1..89e857eec0bb30 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx
@@ -157,7 +157,7 @@ export const StepLogistics: React.FunctionComponent = React.memo(
getFormData,
} = form;
- const [{ addMeta }] = useFormData({
+ const [{ addMeta }] = useFormData<{ addMeta: boolean }>({
form,
watch: 'addMeta',
});
diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts
index 3767144a1b7984..d49755b6f68c16 100644
--- a/x-pack/plugins/infra/server/features.ts
+++ b/x-pack/plugins/infra/server/features.ts
@@ -17,8 +17,6 @@ export const METRICS_FEATURE = {
}),
order: 800,
category: DEFAULT_APP_CATEGORIES.observability,
- icon: 'metricsApp',
- navLinkId: 'metrics',
app: ['infra', 'metrics', 'kibana'],
catalogue: ['infraops', 'metrics'],
management: {
@@ -68,8 +66,6 @@ export const LOGS_FEATURE = {
}),
order: 700,
category: DEFAULT_APP_CATEGORIES.observability,
- icon: 'logsApp',
- navLinkId: 'logs',
app: ['infra', 'logs', 'kibana'],
catalogue: ['infralogging', 'logs'],
alerting: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID],
diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts
index e3757b46ed715e..e5a06b7e38131f 100644
--- a/x-pack/plugins/ingest_manager/server/plugin.ts
+++ b/x-pack/plugins/ingest_manager/server/plugin.ts
@@ -181,9 +181,7 @@ export class IngestManagerPlugin
deps.features.registerKibanaFeature({
id: PLUGIN_ID,
name: 'Ingest Manager',
- icon: 'savedObjectsApp',
category: DEFAULT_APP_CATEGORIES.management,
- navLinkId: PLUGIN_ID,
app: [PLUGIN_ID, 'kibana'],
catalogue: ['ingestManager'],
privileges: {
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts
index 311b3bbf7f13ba..645ae8880fa61c 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts
@@ -132,6 +132,8 @@ export const createAgentPolicyHandler: RequestHandler<
});
}
+ await agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicy.id);
+
const body: CreateAgentPolicyResponse = {
item: agentPolicy,
};
@@ -185,6 +187,7 @@ export const copyAgentPolicyHandler: RequestHandler<
user: user || undefined,
}
);
+
const body: CopyAgentPolicyResponse = { item: agentPolicy };
return response.ok({
body,
diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
index 8f1ece923f126f..9d85a151efbbf9 100644
--- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
+++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
@@ -6,6 +6,7 @@
import { SavedObjectsServiceSetup, SavedObjectsType } from 'kibana/server';
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
+import { migratePackagePolicyToV7110 } from '../../../security_solution/common';
import {
OUTPUT_SAVED_OBJECT_TYPE,
AGENT_POLICY_SAVED_OBJECT_TYPE,
@@ -268,6 +269,7 @@ const getSavedObjectTypes = (
},
migrations: {
'7.10.0': migratePackagePolicyToV7100,
+ '7.11.0': migratePackagePolicyToV7110,
},
},
[PACKAGES_SAVED_OBJECT_TYPE]: {
diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts
index 75c16df483a76e..31bf3e72077748 100644
--- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts
@@ -34,6 +34,7 @@ import { agentPolicyUpdateEventHandler } from './agent_policy_update';
import { getSettings } from './settings';
import { normalizeKuery, escapeSearchQueryPhrase } from './saved_object';
import { getFullAgentPolicyKibanaConfig } from '../../common/services/full_agent_policy_kibana_config';
+import { isAgentsSetup } from './agents/setup';
const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE;
@@ -287,6 +288,8 @@ class AgentPolicyService {
throw new Error('Copied agent policy not found');
}
+ await this.createFleetPolicyChangeAction(soClient, newAgentPolicy.id);
+
return updatedAgentPolicy;
}
@@ -433,6 +436,11 @@ class AgentPolicyService {
soClient: SavedObjectsClientContract,
agentPolicyId: string
) {
+ // If Agents is not setup skip the creation of POLICY_CHANGE agent actions
+ // the action will be created during the fleet setup
+ if (!(await isAgentsSetup(soClient))) {
+ return;
+ }
const policy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId);
if (!policy || !policy.revision) {
return;
diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts
index 4ddb3cfb6a6a52..fe06de765bbff6 100644
--- a/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts
@@ -6,8 +6,7 @@
import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server';
import { generateEnrollmentAPIKey, deleteEnrollmentApiKeyForAgentPolicyId } from './api_keys';
-import { unenrollForAgentPolicyId } from './agents';
-import { outputService } from './output';
+import { isAgentsSetup, unenrollForAgentPolicyId } from './agents';
import { agentPolicyService } from './agent_policy';
import { appContextService } from './app_context';
@@ -31,11 +30,8 @@ export async function agentPolicyUpdateEventHandler(
action: string,
agentPolicyId: string
) {
- const adminUser = await outputService.getAdminUser(soClient);
- const outputId = await outputService.getDefaultOutputId(soClient);
-
- // If no admin user and no default output fleet is not enabled just skip this hook
- if (!adminUser || !outputId) {
+ // If Agents are not setup skip this hook
+ if (!(await isAgentsSetup(soClient))) {
return;
}
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/index.ts
index c878b666bde886..0800a04fb1b3ba 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/index.ts
@@ -16,3 +16,4 @@ export * from './update';
export * from './actions';
export * from './reassign';
export * from './authenticate';
+export * from './setup';
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/setup.ts b/x-pack/plugins/ingest_manager/server/services/agents/setup.ts
new file mode 100644
index 00000000000000..1393e732e89d10
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/agents/setup.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsClientContract } from 'src/core/server';
+import { SO_SEARCH_LIMIT } from '../../constants';
+import { agentPolicyService } from '../agent_policy';
+import { outputService } from '../output';
+import { getLatestConfigChangeAction } from './actions';
+
+export async function isAgentsSetup(soClient: SavedObjectsClientContract): Promise {
+ const adminUser = await outputService.getAdminUser(soClient, false);
+ const outputId = await outputService.getDefaultOutputId(soClient);
+ // If admin user (fleet_enroll) and output id exist Agents are correctly setup
+ return adminUser && outputId ? true : false;
+}
+
+/**
+ * During the migration from 7.9 to 7.10 we introduce a new agent action POLICY_CHANGE per policy
+ * this function ensure that action exist for each policy
+ *
+ * @param soClient
+ */
+export async function ensureAgentActionPolicyChangeExists(soClient: SavedObjectsClientContract) {
+ // If Agents are not setup skip
+ if (!(await isAgentsSetup(soClient))) {
+ return;
+ }
+
+ const { items: agentPolicies } = await agentPolicyService.list(soClient, {
+ perPage: SO_SEARCH_LIMIT,
+ });
+
+ await Promise.all(
+ agentPolicies.map(async (agentPolicy) => {
+ const policyChangeActionExist = !!(await getLatestConfigChangeAction(
+ soClient,
+ agentPolicy.id
+ ));
+
+ if (!policyChangeActionExist) {
+ return agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicy.id);
+ }
+ })
+ );
+}
diff --git a/x-pack/plugins/ingest_manager/server/services/output.ts b/x-pack/plugins/ingest_manager/server/services/output.ts
index f780bd95faedce..0e3b652422faae 100644
--- a/x-pack/plugins/ingest_manager/server/services/output.ts
+++ b/x-pack/plugins/ingest_manager/server/services/output.ts
@@ -65,8 +65,8 @@ class OutputService {
return outputs.saved_objects[0].id;
}
- public async getAdminUser(soClient: SavedObjectsClientContract) {
- if (cachedAdminUser) {
+ public async getAdminUser(soClient: SavedObjectsClientContract, useCache = true) {
+ if (useCache && cachedAdminUser) {
return cachedAdminUser;
}
diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts
index c7ecf843d6a51e..7f379d3ea4f13d 100644
--- a/x-pack/plugins/ingest_manager/server/services/setup.ts
+++ b/x-pack/plugins/ingest_manager/server/services/setup.ts
@@ -29,6 +29,7 @@ import { generateEnrollmentAPIKey } from './api_keys';
import { settingsService } from '.';
import { awaitIfPending } from './setup_utils';
import { createDefaultSettings } from './settings';
+import { ensureAgentActionPolicyChangeExists } from './agents';
const FLEET_ENROLL_USERNAME = 'fleet_enroll';
const FLEET_ENROLL_ROLE = 'fleet_enroll';
@@ -80,6 +81,7 @@ async function createSetupSideEffects(
) {
throw new Error('Policy not found');
}
+
for (const installedPackage of installedPackages) {
const packageShouldBeInstalled = DEFAULT_AGENT_POLICIES_PACKAGES.some(
(packageName) => installedPackage.name === packageName
@@ -105,6 +107,8 @@ async function createSetupSideEffects(
}
}
+ await ensureAgentActionPolicyChangeExists(soClient);
+
return { isIntialized: true };
}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx
index f01228a26297b2..841654fcc429e6 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx
@@ -6,7 +6,7 @@
import React, { FunctionComponent, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
-
+import { get } from 'lodash';
import {
FIELD_TYPES,
UseField,
@@ -44,8 +44,8 @@ export const Json: FunctionComponent = () => {
const form = useFormContext();
const [isAddToPathDisabled, setIsAddToPathDisabled] = useState(false);
useEffect(() => {
- const subscription = form.subscribe(({ data: { raw: rawData } }) => {
- const hasTargetField = !!rawData[TARGET_FIELD_PATH];
+ const subscription = form.subscribe(({ data: { internal } }) => {
+ const hasTargetField = !!get(internal, TARGET_FIELD_PATH);
if (hasTargetField && !isAddToPathDisabled) {
setIsAddToPathDisabled(true);
form.getFields()[ADD_TO_ROOT_FIELD_PATH].setValue(false);
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx
index aa9e2879aaddfa..f1d7f7a4868948 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx
@@ -21,6 +21,7 @@ import { Document } from '../../types';
import { Tabs, TestPipelineFlyoutTab, OutputTab, DocumentsTab } from './test_pipeline_tabs';
import { TestPipelineFlyoutForm } from './test_pipeline_flyout.container';
+
export interface Props {
onClose: () => void;
handleTestPipeline: (
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx
index cd82e0f4ff5ca1..6888f947b86067 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx
@@ -21,7 +21,6 @@ import {
ValidationFuncArg,
FormHook,
Form,
- useFormData,
} from '../../../../../../../shared_imports';
import { Document } from '../../../../types';
import { AddDocumentsAccordion } from './add_documents_accordion';
@@ -149,16 +148,15 @@ export const DocumentsTab: FunctionComponent = ({
resetTestOutput,
}) => {
const { services } = useKibana();
-
- const [, formatData] = useFormData({ form });
+ const { getFormData, reset } = form;
const onAddDocumentHandler = useCallback(
(document) => {
- const { documents: existingDocuments = [] } = formatData();
+ const { documents: existingDocuments = [] } = getFormData();
- form.reset({ defaultValue: { documents: [...existingDocuments, document] } });
+ reset({ defaultValue: { documents: [...existingDocuments, document] } });
},
- [form, formatData]
+ [reset, getFormData]
);
const [showResetModal, setShowResetModal] = useState(false);
diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
index 25813aa9656976..edee2bf8e6b1dc 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
@@ -696,6 +696,9 @@ describe('Lens App', () => {
undefined
);
expect(props.redirectTo).toHaveBeenCalledWith('aaa');
+ expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith(
+ "Saved 'hello there'"
+ );
});
it('adds to the recently accessed list on save', async () => {
@@ -730,6 +733,9 @@ describe('Lens App', () => {
component.setProps({ initialInput: { savedObjectId: 'aaa' } });
});
expect(services.attributeService.wrapAttributes).toHaveBeenCalledTimes(1);
+ expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith(
+ "Saved 'hello there'"
+ );
});
it('saves existing docs', async () => {
@@ -751,6 +757,9 @@ describe('Lens App', () => {
component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } });
});
expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1);
+ expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith(
+ "Saved 'hello there'"
+ );
});
it('handles save failure by showing a warning, but still allows another save', async () => {
diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx
index 3407ea5de49c46..2d35c203661d05 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.tsx
@@ -411,6 +411,15 @@ export function App({
return;
}
+ notifications.toasts.addSuccess(
+ i18n.translate('xpack.lens.app.saveVisualization.successNotificationText', {
+ defaultMessage: `Saved '{visTitle}'`,
+ values: {
+ visTitle: docToSave.title,
+ },
+ })
+ );
+
if (
attributeService.inputIsRefType(newInput) &&
newInput.savedObjectId !== originalSavedObjectId
diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts
index 9fee72b59b44cd..83cdbd62f3484b 100644
--- a/x-pack/plugins/lens/server/usage/task.ts
+++ b/x-pack/plugins/lens/server/usage/task.ts
@@ -48,7 +48,6 @@ function registerLensTelemetryTask(
taskManager.registerTaskDefinitions({
[TELEMETRY_TASK_TYPE]: {
title: 'Lens usage fetch task',
- type: TELEMETRY_TASK_TYPE,
timeout: '1m',
createTaskRunner: telemetryTaskRunner(logger, core, config),
},
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
index 429f6c7af6e274..27676422543220 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
@@ -120,7 +120,9 @@ exports[`UploadLicense should display a modal when license requires acknowledgem
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -171,7 +173,9 @@ exports[`UploadLicense should display a modal when license requires acknowledgem
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -213,7 +217,9 @@ exports[`UploadLicense should display a modal when license requires acknowledgem
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -1035,22 +1041,30 @@ exports[`UploadLicense should display an error when ES says license is expired 1
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -1113,22 +1127,30 @@ exports[`UploadLicense should display an error when ES says license is expired 1
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -1182,22 +1204,30 @@ exports[`UploadLicense should display an error when ES says license is expired 1
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -1723,22 +1753,30 @@ exports[`UploadLicense should display an error when ES says license is invalid 1
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -1801,22 +1839,30 @@ exports[`UploadLicense should display an error when ES says license is invalid 1
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -1870,22 +1916,30 @@ exports[`UploadLicense should display an error when ES says license is invalid 1
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -2411,22 +2465,30 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`]
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -2489,22 +2551,30 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`]
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -2558,22 +2628,30 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`]
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -3099,22 +3177,30 @@ exports[`UploadLicense should display error when ES returns error 1`] = `
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -3177,22 +3263,30 @@ exports[`UploadLicense should display error when ES returns error 1`] = `
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
@@ -3246,22 +3340,30 @@ exports[`UploadLicense should display error when ES returns error 1`] = `
"calls": Array [
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
Array [
Object {
+ "hash": "",
"pathname": "/home",
+ "search": "",
},
],
],
diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts
index 50196a1a0bcc70..2fafe9de59fdb1 100644
--- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts
+++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts
@@ -23,7 +23,7 @@ export type ReturnExceptionListAndItems = [
* Hook for using to get an ExceptionList and it's ExceptionListItems
*
* @param http Kibana http service
- * @param lists array of ExceptionIdentifiers for all lists to fetch
+ * @param lists array of ExceptionListIdentifiers for all lists to fetch
* @param onError error callback
* @param onSuccess callback when all lists fetched successfully
* @param filterOptions optional - filter by fields or tags
diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts
index ac21288848154c..64e4efb5daad27 100644
--- a/x-pack/plugins/lists/public/exceptions/types.ts
+++ b/x-pack/plugins/lists/public/exceptions/types.ts
@@ -50,7 +50,7 @@ export interface UseExceptionListSuccess {
export interface UseExceptionListProps {
http: HttpStart;
- lists: ExceptionIdentifiers[];
+ lists: ExceptionListIdentifiers[];
onError?: (arg: string[]) => void;
filterOptions: FilterExceptionsOptions[];
pagination?: Pagination;
@@ -60,7 +60,7 @@ export interface UseExceptionListProps {
onSuccess?: (arg: UseExceptionListSuccess) => void;
}
-export interface ExceptionIdentifiers {
+export interface ExceptionListIdentifiers {
id: string;
listId: string;
namespaceType: NamespaceType;
@@ -91,7 +91,7 @@ export interface ApiCallMemoProps {
}
export interface ApiCallFindListsItemsMemoProps {
- lists: ExceptionIdentifiers[];
+ lists: ExceptionListIdentifiers[];
filterOptions: FilterExceptionsOptions[];
pagination: Partial;
showDetectionsListsOnly: boolean;
diff --git a/x-pack/plugins/lists/public/exceptions/utils.ts b/x-pack/plugins/lists/public/exceptions/utils.ts
index 2acb690d3822c2..3a5984956b0d0a 100644
--- a/x-pack/plugins/lists/public/exceptions/utils.ts
+++ b/x-pack/plugins/lists/public/exceptions/utils.ts
@@ -6,14 +6,14 @@
import { NamespaceType } from '../../common/schemas';
-import { ExceptionIdentifiers } from './types';
+import { ExceptionListIdentifiers } from './types';
export const getIdsAndNamespaces = ({
lists,
showDetection,
showEndpoint,
}: {
- lists: ExceptionIdentifiers[];
+ lists: ExceptionListIdentifiers[];
showDetection: boolean;
showEndpoint: boolean;
}): { ids: string[]; namespaces: NamespaceType[] } =>
diff --git a/x-pack/plugins/lists/public/shared_exports.ts b/x-pack/plugins/lists/public/shared_exports.ts
index 16026a436f1549..b8293971d14dff 100644
--- a/x-pack/plugins/lists/public/shared_exports.ts
+++ b/x-pack/plugins/lists/public/shared_exports.ts
@@ -30,7 +30,7 @@ export {
} from './exceptions/api';
export {
ExceptionList,
- ExceptionIdentifiers,
+ ExceptionListIdentifiers,
Pagination,
UseExceptionListSuccess,
} from './exceptions/types';
diff --git a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx
index d66a0d1b7d0dae..3b0891a4fd44df 100644
--- a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx
+++ b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx
@@ -42,6 +42,7 @@ import {
getNavigateToApp,
} from '../../../kibana_services';
import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader';
+import { getAppTitle } from '../../../../common/i18n_getters';
export const EMPTY_FILTER = '';
@@ -101,7 +102,8 @@ export class MapsListView extends React.Component {
async initMapList() {
this.fetchItems();
addHelpMenuToAppChrome();
- getCoreChrome().docTitle.change('Maps');
+ getCoreChrome().docTitle.change(getAppTitle());
+ getCoreChrome().setBreadcrumbs([{ text: getAppTitle() }]);
}
_find = (search: string) => getMapsSavedObjectLoader().find(search, this.state.listingLimit);
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
index 149c04b414c180..1f74b0d6d14498 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
@@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { getNavigateToApp } from '../../../kibana_services';
import { goToSpecifiedPath } from '../../maps_router';
+import { getAppTitle } from '../../../../common/i18n_getters';
export const unsavedChangesWarning = i18n.translate(
'xpack.maps.breadCrumbs.unsavedChangesWarning',
@@ -37,9 +38,7 @@ export function getBreadcrumbs({
}
breadcrumbs.push({
- text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', {
- defaultMessage: 'Maps',
- }),
+ text: getAppTitle(),
onClick: () => {
if (getHasUnsavedChanges()) {
const navigateAway = window.confirm(unsavedChangesWarning);
diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts
index 46e39fcdac27a2..00950e96047a0e 100644
--- a/x-pack/plugins/maps/server/plugin.ts
+++ b/x-pack/plugins/maps/server/plugin.ts
@@ -171,8 +171,6 @@ export class MapsPlugin implements Plugin {
}),
order: 400,
category: DEFAULT_APP_CATEGORIES.kibana,
- icon: APP_ICON,
- navLinkId: APP_ID,
app: [APP_ID, 'kibana'],
catalogue: [APP_ID],
privileges: {
diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts
index 5d0ecf96fb6b5f..cbf2acd1524766 100644
--- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts
+++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts
@@ -19,7 +19,9 @@ export type DataFrameAnalyticsId = string;
export interface OutlierAnalysis {
[key: string]: {};
- outlier_detection: {};
+ outlier_detection: {
+ compute_feature_influence?: boolean;
+ };
}
interface Regression {
diff --git a/x-pack/plugins/ml/common/util/analytics_utils.ts b/x-pack/plugins/ml/common/util/analytics_utils.ts
index d231ed43443892..94797efdfcfadf 100644
--- a/x-pack/plugins/ml/common/util/analytics_utils.ts
+++ b/x-pack/plugins/ml/common/util/analytics_utils.ts
@@ -13,16 +13,19 @@ import {
import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics';
export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => {
+ if (typeof arg !== 'object' || arg === null) return false;
const keys = Object.keys(arg);
return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION;
};
export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => {
+ if (typeof arg !== 'object' || arg === null) return false;
const keys = Object.keys(arg);
return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION;
};
export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => {
+ if (typeof arg !== 'object' || arg === null) return false;
const keys = Object.keys(arg);
return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts
index f88694a1952b2c..642d0ae564b859 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts
+++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts
@@ -33,7 +33,6 @@ import {
import {
FEATURE_IMPORTANCE,
- FEATURE_INFLUENCE,
OUTLIER_SCORE,
TOP_CLASSES,
} from '../../data_frame_analytics/common/constants';
@@ -112,10 +111,7 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results
schema = NON_AGGREGATABLE;
}
- if (
- field === `${resultsField}.${OUTLIER_SCORE}` ||
- field.includes(`${resultsField}.${FEATURE_INFLUENCE}`)
- ) {
+ if (field === `${resultsField}.${OUTLIER_SCORE}`) {
schema = 'numeric';
}
@@ -203,11 +199,6 @@ export const useRenderCellValue = (
}
function getCellValue(cId: string) {
- if (cId.includes(`.${FEATURE_INFLUENCE}.`) && resultsField !== undefined) {
- const results = getNestedProperty(tableItems[adjustedRowIndex], resultsField, null);
- return results[cId.replace(`${resultsField}.`, '')];
- }
-
if (tableItems.hasOwnProperty(adjustedRowIndex)) {
const item = tableItems[adjustedRowIndex];
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
index e4581f0a87bddc..c606cbd1cc11ac 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
@@ -17,7 +17,7 @@ import {
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public';
import { newJobCapsService } from '../../services/new_job_capabilities_service';
-import { FEATURE_IMPORTANCE, FEATURE_INFLUENCE, OUTLIER_SCORE, TOP_CLASSES } from './constants';
+import { FEATURE_IMPORTANCE, OUTLIER_SCORE, TOP_CLASSES } from './constants';
import { DataFrameAnalyticsConfig } from '../../../../common/types/data_frame_analytics';
export type EsId = string;
@@ -179,7 +179,6 @@ export const getDefaultFieldsFromJobCaps = (
const resultsField = jobConfig.dest.results_field;
const featureImportanceFields = [];
- const featureInfluenceFields = [];
const topClassesFields = [];
const allFields: any = [];
let type: ES_FIELD_TYPES | undefined;
@@ -193,16 +192,6 @@ export const getDefaultFieldsFromJobCaps = (
name: `${resultsField}.${OUTLIER_SCORE}`,
type: KBN_FIELD_TYPES.NUMBER,
});
-
- featureInfluenceFields.push(
- ...fields
- .filter((d) => !jobConfig.analyzed_fields.excludes.includes(d.id))
- .map((d) => ({
- id: `${resultsField}.${FEATURE_INFLUENCE}.${d.id}`,
- name: `${resultsField}.${FEATURE_INFLUENCE}.${d.name}`,
- type: KBN_FIELD_TYPES.NUMBER,
- }))
- );
}
}
@@ -247,12 +236,7 @@ export const getDefaultFieldsFromJobCaps = (
}
}
- allFields.push(
- ...fields,
- ...featureImportanceFields,
- ...featureInfluenceFields,
- ...topClassesFields
- );
+ allFields.push(...fields, ...featureImportanceFields, ...topClassesFields);
allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) =>
sortExplorationResultsFields(a, b, jobConfig)
);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts
index 667dea27de96e3..8e50aab0914db4 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts
@@ -19,7 +19,8 @@ import { DataFrameAnalyticsConfig } from '../../../../common/types/data_frame_an
export const getIndexData = async (
jobConfig: DataFrameAnalyticsConfig | undefined,
dataGrid: UseDataGridReturnType,
- searchQuery: SavedSearchQuery
+ searchQuery: SavedSearchQuery,
+ options: { didCancel: boolean }
) => {
if (jobConfig !== undefined) {
const {
@@ -52,7 +53,7 @@ export const getIndexData = async (
index: jobConfig.dest.index,
body: {
fields: ['*'],
- _source: jobConfig.dest.results_field,
+ _source: [],
query: searchQuery,
from: pageIndex * pageSize,
size: pageSize,
@@ -60,14 +61,11 @@ export const getIndexData = async (
},
});
- setRowCount(resp.hits.total.value);
- const docs = resp.hits.hits.map((d) => ({
- ...getProcessedFields(d.fields),
- [jobConfig.dest.results_field]: d._source[jobConfig.dest.results_field],
- }));
-
- setTableItems(docs);
- setStatus(INDEX_STATUS.LOADED);
+ if (!options.didCancel) {
+ setRowCount(resp.hits.total.value);
+ setTableItems(resp.hits.hits.map((d) => getProcessedFields(d.fields)));
+ setStatus(INDEX_STATUS.LOADED);
+ }
} catch (e) {
setErrorMessage(extractErrorMessage(e));
setStatus(INDEX_STATUS.ERROR);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts
index 7d2ca86a38083d..81c2e246120c01 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts
@@ -6,6 +6,8 @@
import { useEffect, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+
import { IndexPattern } from '../../../../../../../src/plugins/data/public';
import { extractErrorMessage } from '../../../../common/util/errors';
@@ -32,6 +34,9 @@ export const useResultsViewConfig = (jobId: string) => {
const trainedModelsApiService = useTrainedModelsApiService();
const [indexPattern, setIndexPattern] = useState(undefined);
+ const [indexPatternErrorMessage, setIndexPatternErrorMessage] = useState(
+ undefined
+ );
const [isInitialized, setIsInitialized] = useState(false);
const [needsDestIndexPattern, setNeedsDestIndexPattern] = useState(false);
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false);
@@ -105,7 +110,11 @@ export const useResultsViewConfig = (jobId: string) => {
setNeedsDestIndexPattern(true);
const sourceIndex = jobConfigUpdate.source.index[0];
const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex;
- indexP = await mlContext.indexPatterns.get(sourceIndexPatternId);
+ try {
+ indexP = await mlContext.indexPatterns.get(sourceIndexPatternId);
+ } catch (e) {
+ indexP = undefined;
+ }
}
if (indexP !== undefined) {
@@ -114,6 +123,16 @@ export const useResultsViewConfig = (jobId: string) => {
setIndexPattern(indexP);
setIsInitialized(true);
setIsLoadingJobConfig(false);
+ } else {
+ setIndexPatternErrorMessage(
+ i18n.translate(
+ 'xpack.ml.dataframe.analytics.results.indexPatternsMissingErrorMessage',
+ {
+ defaultMessage:
+ 'To view this page, a Kibana index pattern is necessary for either the destination or source index of this analytics job.',
+ }
+ )
+ );
}
} catch (e) {
setJobCapsServiceErrorMessage(extractErrorMessage(e));
@@ -129,6 +148,7 @@ export const useResultsViewConfig = (jobId: string) => {
return {
indexPattern,
+ indexPatternErrorMessage,
isInitialized,
isLoadingJobConfig,
jobCapsServiceErrorMessage,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx
index de4d1a97f248fd..cdecead21d4dee 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx
@@ -6,7 +6,7 @@
import React, { FC, useEffect, useState } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
+import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useUrlState } from '../../../../../util/url_state';
@@ -70,6 +70,7 @@ export const ExplorationPageWrapper: FC = ({
}) => {
const {
indexPattern,
+ indexPatternErrorMessage,
isInitialized,
isLoadingJobConfig,
jobCapsServiceErrorMessage,
@@ -99,6 +100,22 @@ export const ExplorationPageWrapper: FC = ({
}
}, [jobConfig?.dest.results_field]);
+ if (indexPatternErrorMessage !== undefined) {
+ return (
+
+
+ {indexPatternErrorMessage}
+
+
+ );
+ }
+
if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) {
return (
{
- getIndexData(jobConfig, dataGrid, searchQuery);
+ const options = { didCancel: false };
+ getIndexData(jobConfig, dataGrid, searchQuery, options);
+ return () => {
+ options.didCancel = true;
+ };
// custom comparison
}, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts
index d1889a8acb990d..1ce3b3528e44b8 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts
@@ -19,11 +19,8 @@ export const getFeatureCount = (resultsField: string, tableItems: DataGridItem[]
const fullItem = tableItems[0];
- if (
- fullItem[resultsField] !== undefined &&
- Array.isArray(fullItem[resultsField][FEATURE_INFLUENCE])
- ) {
- return fullItem[resultsField][FEATURE_INFLUENCE].length;
+ if (Array.isArray(fullItem[`${resultsField}.${FEATURE_INFLUENCE}.feature_name`])) {
+ return fullItem[`${resultsField}.${FEATURE_INFLUENCE}.feature_name`].length;
}
return 0;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx
index 8fc2486599755f..9e30ed3cdfe95f 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx
@@ -6,7 +6,9 @@
import React, { useState, FC } from 'react';
-import { EuiSpacer, EuiText } from '@elastic/eui';
+import { EuiCallOut, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
import {
useColorRange,
@@ -15,7 +17,8 @@ import {
} from '../../../../../components/color_range_legend';
import { SavedSearchQuery } from '../../../../../contexts/ml';
-import { defaultSearchQuery, useResultsViewConfig } from '../../../../common';
+import { defaultSearchQuery, isOutlierAnalysis, useResultsViewConfig } from '../../../../common';
+import { FEATURE_INFLUENCE } from '../../../../common/constants';
import { ExpandableSectionAnalytics, ExpandableSectionResults } from '../expandable_section';
import { ExplorationQueryBar } from '../exploration_query_bar';
@@ -30,17 +33,54 @@ interface ExplorationProps {
}
export const OutlierExploration: FC = React.memo(({ jobId }) => {
- const { indexPattern, jobConfig, needsDestIndexPattern } = useResultsViewConfig(jobId);
+ const {
+ indexPattern,
+ indexPatternErrorMessage,
+ jobConfig,
+ needsDestIndexPattern,
+ } = useResultsViewConfig(jobId);
const [searchQuery, setSearchQuery] = useState(defaultSearchQuery);
const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery);
const { columnsWithCharts, tableItems } = outlierData;
- const colorRange = useColorRange(
- COLOR_RANGE.BLUE,
- COLOR_RANGE_SCALE.INFLUENCER,
- jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1
- );
+ const featureCount = getFeatureCount(jobConfig?.dest?.results_field || '', tableItems);
+ const colorRange = useColorRange(COLOR_RANGE.BLUE, COLOR_RANGE_SCALE.INFLUENCER, featureCount);
+
+ // Show the color range only if feature influence is enabled and there's more than 0 features.
+ const showColorRange =
+ featureCount > 0 &&
+ isOutlierAnalysis(jobConfig?.analysis) &&
+ jobConfig?.analysis.outlier_detection.compute_feature_influence === true;
+
+ const resultsField = jobConfig?.dest.results_field ?? '';
+
+ // Identify if the results index has a legacy feature influence format.
+ // If feature influence was enabled for the legacy job we'll show a callout
+ // with some additional information for a workaround.
+ const showLegacyFeatureInfluenceFormatCallout =
+ !needsDestIndexPattern &&
+ isOutlierAnalysis(jobConfig?.analysis) &&
+ jobConfig?.analysis.outlier_detection.compute_feature_influence === true &&
+ columnsWithCharts.findIndex(
+ (d) => d.id === `${resultsField}.${FEATURE_INFLUENCE}.feature_name`
+ ) === -1;
+
+ if (indexPatternErrorMessage !== undefined) {
+ return (
+
+
+ {indexPatternErrorMessage}
+
+
+ );
+ }
return (
<>
@@ -58,8 +98,26 @@ export const OutlierExploration: FC = React.memo(({ jobId }) =
>
)}
{typeof jobConfig?.id === 'string' && }
+ {showLegacyFeatureInfluenceFormatCallout && (
+ <>
+
+
+ >
+ )}
{
- getIndexData(jobConfig, dataGrid, searchQuery);
+ const options = { didCancel: false };
+ getIndexData(jobConfig, dataGrid, searchQuery, options);
+ return () => {
+ options.didCancel = true;
+ };
// custom comparison
}, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]);
@@ -151,19 +153,17 @@ export const useOutlierData = (
const split = columnId.split('.');
let backgroundColor;
+ const featureNames = fullItem[`${resultsField}.${FEATURE_INFLUENCE}.feature_name`];
+
// column with feature values get color coded by its corresponding influencer value
- if (
- fullItem[resultsField] !== undefined &&
- fullItem[resultsField][FEATURE_INFLUENCE] !== undefined &&
- fullItem[resultsField][FEATURE_INFLUENCE].find(
- (d: FeatureInfluence) => d.feature_name === columnId
- ) !== undefined
- ) {
- backgroundColor = colorRange(
- fullItem[resultsField][FEATURE_INFLUENCE].find(
- (d: FeatureInfluence) => d.feature_name === columnId
- ).influence
- );
+ if (Array.isArray(featureNames)) {
+ const featureIndex = featureNames.indexOf(columnId);
+
+ if (featureIndex > -1) {
+ backgroundColor = colorRange(
+ fullItem[`${resultsField}.${FEATURE_INFLUENCE}.influence`][featureIndex]
+ );
+ }
}
// column with influencer values get color coded by its own value
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
index 7d715839772046..7d2c7d60e3ee6e 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
@@ -58,7 +58,12 @@ const emptyOption: EuiComboBoxOptionOption = {
label: '',
};
-const excludeFrequentOptions: EuiComboBoxOptionOption[] = [{ label: 'all' }, { label: 'none' }];
+const excludeFrequentOptions: EuiComboBoxOptionOption[] = [
+ { label: 'all' },
+ { label: 'none' },
+ { label: 'by' },
+ { label: 'over' },
+];
export const AdvancedDetectorModal: FC = ({
payload,
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx
index 54fd8ba0359581..280ac85a5a2bc2 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx
@@ -142,7 +142,7 @@ export const ExcludeFrequentDescription: FC = memo(({ children }) => {
description={
}
>
diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts
index d0c837b7a1caf7..1592973022dbd1 100644
--- a/x-pack/plugins/ml/server/plugin.ts
+++ b/x-pack/plugins/ml/server/plugin.ts
@@ -17,7 +17,7 @@ import {
} from 'kibana/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PluginsSetup, RouteInitialization } from './types';
-import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app';
+import { PLUGIN_ID } from '../common/constants/app';
import { MlCapabilities } from '../common/types/capabilities';
import { initMlTelemetry } from './lib/telemetry';
@@ -73,10 +73,8 @@ export class MlServerPlugin implements Plugin {
expect(queryAllByText('View in app')).toEqual([]);
expect(getByText('elastic-co-frontend')).toBeInTheDocument();
});
+ it('shows empty state', () => {
+ jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
+ data: undefined,
+ status: fetcherHook.FETCH_STATUS.SUCCESS,
+ refetch: jest.fn(),
+ });
+ const { getByText, queryAllByText, getAllByText } = render(
+
+ );
+
+ expect(getByText('User Experience')).toBeInTheDocument();
+ expect(getAllByText('No data is available.')).toHaveLength(3);
+ expect(queryAllByText('View in app')).toEqual([]);
+ expect(getByText('elastic-co-frontend')).toBeInTheDocument();
+ });
});
diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx
index e5b8d2c243617e..0d0a388855ff24 100644
--- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx
+++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx
@@ -4,7 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiIconTip, euiPaletteForStatus, EuiSpacer, EuiStat } from '@elastic/eui';
+import {
+ EuiCard,
+ EuiFlexGroup,
+ EuiIconTip,
+ euiPaletteForStatus,
+ EuiSpacer,
+ EuiStat,
+} from '@elastic/eui';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { PaletteLegends } from './palette_legends';
@@ -14,6 +21,7 @@ import {
CV_GOOD_LABEL,
LESS_LABEL,
MORE_LABEL,
+ NO_DATA,
CV_POOR_LABEL,
IS_LABEL,
TAKES_LABEL,
@@ -26,7 +34,7 @@ export interface Thresholds {
interface Props {
title: string;
- value: string;
+ value?: string;
ranks?: number[];
loading: boolean;
thresholds: Thresholds;
@@ -80,6 +88,9 @@ export function CoreVitalItem({
const biggestValIndex = ranks.indexOf(Math.max(...ranks));
+ if (value === undefined && ranks[0] === 100 && !loading) {
+ return ;
+ }
return (
<>
)}
- {hasData?.ux && (
+ {(hasData.ux as UXHasDataResponse).hasData && (
;
@@ -88,6 +88,8 @@ export function OverviewPage({ routeParams }: Props) {
const appEmptySections = getEmptySections({ core }).filter(({ id }) => {
if (id === 'alert') {
return alertStatus !== FETCH_STATUS.FAILURE && !alerts.length;
+ } else if (id === 'ux') {
+ return !(hasData[id] as UXHasDataResponse).hasData;
}
return !hasData[id];
});
diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts
index c501ad82954a3d..0663c239c61ba6 100644
--- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts
+++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts
@@ -19,8 +19,6 @@ export const createFeature = (
) => {
const { excludeFromBaseAll, excludeFromBaseRead, privileges, category, ...rest } = config;
return new KibanaFeature({
- icon: 'discoverApp',
- navLinkId: 'discover',
app: [],
category: category ?? { id: 'foo', label: 'foo' },
catalogue: [],
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx
index 7dff2912e6aa3d..e6daa15fe7e6e0 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx
@@ -30,7 +30,6 @@ const buildFeatures = () => {
new KibanaFeature({
id: 'feature1',
name: 'Feature 1',
- icon: 'addDataApp',
app: ['feature1App'],
category: { id: 'foo', label: 'foo' },
privileges: {
@@ -55,7 +54,6 @@ const buildFeatures = () => {
new KibanaFeature({
id: 'feature2',
name: 'Feature 2',
- icon: 'addDataApp',
app: ['feature2App'],
category: { id: 'foo', label: 'foo' },
privileges: {
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx
index 77b6da2a004871..6601c6ae1f8d55 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx
@@ -19,7 +19,6 @@ const buildProps = (customProps: any = {}) => {
name: 'Feature 1',
app: ['app'],
category: { id: 'foo', label: 'foo' },
- icon: 'spacesApp',
privileges: {
all: {
app: ['app'],
diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts
index ea24560c8ddc9b..ac97822a403af0 100644
--- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts
+++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts
@@ -81,7 +81,6 @@ describe('usingPrivileges', () => {
name: 'Foo KibanaFeature',
app: ['fooApp', 'foo'],
category: { id: 'foo', label: 'foo' },
- navLinkId: 'foo',
privileges: null,
}),
],
@@ -170,7 +169,6 @@ describe('usingPrivileges', () => {
name: 'Foo KibanaFeature',
app: ['foo'],
category: { id: 'foo', label: 'foo' },
- navLinkId: 'foo',
privileges: null,
}),
],
@@ -322,7 +320,6 @@ describe('usingPrivileges', () => {
new KibanaFeature({
id: 'fooFeature',
name: 'Foo KibanaFeature',
- navLinkId: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
@@ -330,7 +327,6 @@ describe('usingPrivileges', () => {
new KibanaFeature({
id: 'barFeature',
name: 'Bar KibanaFeature',
- navLinkId: 'bar',
app: ['bar'],
category: { id: 'foo', label: 'foo' },
privileges: null,
@@ -471,7 +467,6 @@ describe('usingPrivileges', () => {
new KibanaFeature({
id: 'fooFeature',
name: 'Foo KibanaFeature',
- navLinkId: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
@@ -479,7 +474,6 @@ describe('usingPrivileges', () => {
new KibanaFeature({
id: 'barFeature',
name: 'Bar KibanaFeature',
- navLinkId: 'bar',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
@@ -559,7 +553,6 @@ describe('all', () => {
name: 'Foo KibanaFeature',
app: ['foo'],
category: { id: 'foo', label: 'foo' },
- navLinkId: 'foo',
privileges: null,
}),
],
diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts
index 89cc9065655cd6..ec975ca5f973f2 100644
--- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts
+++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts
@@ -28,7 +28,6 @@ export function disableUICapabilitiesFactory(
) {
// nav links are sourced from the apps property.
// The Kibana Platform associates nav links to the app which registers it, in a 1:1 relationship.
- // This behavior is replacing the `navLinkId` property.
const featureNavLinkIds = features
.flatMap((feature) => feature.app)
.filter((navLinkId) => navLinkId != null);
diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
index 2b1268b11a0ffa..c7b015b001ccf0 100644
--- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
@@ -18,8 +18,6 @@ describe('features', () => {
new KibanaFeature({
id: 'foo-feature',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
- navLinkId: 'kibana:foo',
app: ['app-1', 'app-2'],
category: { id: 'foo', label: 'foo' },
catalogue: ['catalogue-1', 'catalogue-2'],
@@ -65,7 +63,6 @@ describe('features', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
@@ -169,7 +166,6 @@ describe('features', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
@@ -211,8 +207,6 @@ describe('features', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
- navLinkId: 'kibana:foo',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
@@ -336,8 +330,6 @@ describe('features', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
- navLinkId: 'kibana:foo',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
@@ -421,8 +413,6 @@ describe('features', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
- navLinkId: 'kibana:foo',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
@@ -480,8 +470,6 @@ describe('features', () => {
id: 'foo',
name: 'Foo KibanaFeature',
excludeFromBasePrivileges: true,
- icon: 'arrowDown',
- navLinkId: 'kibana:foo',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
@@ -546,8 +534,6 @@ describe('features', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
- navLinkId: 'kibana:foo',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
@@ -617,8 +603,6 @@ describe('reserved', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
- navLinkId: 'kibana:foo',
app: ['app-1', 'app-2'],
category: { id: 'foo', label: 'foo' },
catalogue: ['catalogue-1', 'catalogue-2'],
@@ -661,7 +645,6 @@ describe('reserved', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
@@ -728,7 +711,6 @@ describe('reserved', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
@@ -770,7 +752,6 @@ describe('subFeatures', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
@@ -899,7 +880,6 @@ describe('subFeatures', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
@@ -1106,7 +1086,6 @@ describe('subFeatures', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
excludeFromBasePrivileges: true,
@@ -1251,7 +1230,6 @@ describe('subFeatures', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
@@ -1419,7 +1397,6 @@ describe('subFeatures', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
excludeFromBasePrivileges: true,
@@ -1551,7 +1528,6 @@ describe('subFeatures', () => {
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
- icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
diff --git a/x-pack/plugins/security/server/session_management/session_management_service.test.ts b/x-pack/plugins/security/server/session_management/session_management_service.test.ts
index 0328455fc83796..155cc0bdd58ff7 100644
--- a/x-pack/plugins/security/server/session_management/session_management_service.test.ts
+++ b/x-pack/plugins/security/server/session_management/session_management_service.test.ts
@@ -50,7 +50,6 @@ describe('SessionManagementService', () => {
expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalledWith({
[SESSION_INDEX_CLEANUP_TASK_NAME]: {
title: 'Cleanup expired or invalid user sessions',
- type: SESSION_INDEX_CLEANUP_TASK_NAME,
createTaskRunner: expect.any(Function),
},
});
diff --git a/x-pack/plugins/security/server/session_management/session_management_service.ts b/x-pack/plugins/security/server/session_management/session_management_service.ts
index 60c0f7c23e959e..fc2e85d683d586 100644
--- a/x-pack/plugins/security/server/session_management/session_management_service.ts
+++ b/x-pack/plugins/security/server/session_management/session_management_service.ts
@@ -78,7 +78,6 @@ export class SessionManagementService {
taskManager.registerTaskDefinitions({
[SESSION_INDEX_CLEANUP_TASK_NAME]: {
title: 'Cleanup expired or invalid user sessions',
- type: SESSION_INDEX_CLEANUP_TASK_NAME,
createTaskRunner: () => ({ run: () => this.sessionIndex.cleanUp() }),
},
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts
index d6352d2e6aa151..8ff75b25388b0f 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts
@@ -1112,6 +1112,7 @@ describe('get_filter', () => {
'@timestamp': {
gte: 'now-5m',
lte: 'now',
+ format: 'strict_date_optional_time',
},
},
},
@@ -1148,6 +1149,7 @@ describe('get_filter', () => {
'event.ingested': {
gte: 'now-5m',
lte: 'now',
+ format: 'strict_date_optional_time',
},
},
},
@@ -1183,6 +1185,7 @@ describe('get_filter', () => {
'@timestamp': {
gte: 'now-5m',
lte: 'now',
+ format: 'strict_date_optional_time',
},
},
},
diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts
index 278ce1d39ae9fb..73638fc48f3813 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts
@@ -117,6 +117,7 @@ export const buildEqlSearchRequest = (
[timestamp]: {
gte: from,
lte: to,
+ format: 'strict_date_optional_time',
},
},
},
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
index 37b73088561960..3250e048edad21 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
@@ -24,6 +24,12 @@ export const factory = (): PolicyConfig => {
malware: {
mode: ProtectionModes.prevent,
},
+ popup: {
+ malware: {
+ message: '',
+ enabled: true,
+ },
+ },
logging: {
file: 'info',
},
@@ -37,6 +43,12 @@ export const factory = (): PolicyConfig => {
malware: {
mode: ProtectionModes.prevent,
},
+ popup: {
+ malware: {
+ message: '',
+ enabled: true,
+ },
+ },
logging: {
file: 'info',
},
diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts
new file mode 100644
index 00000000000000..33cf497e593111
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts
@@ -0,0 +1,168 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from 'kibana/server';
+import { PackagePolicy } from '../../../../../ingest_manager/common';
+import { migratePackagePolicyToV7110 } from './to_v7_11.0';
+
+describe('7.11.0 Endpoint Package Policy migration', () => {
+ const migration = migratePackagePolicyToV7110;
+ it('adds malware notification checkbox and optional message', () => {
+ const doc: SavedObjectUnsanitizedDoc = {
+ attributes: {
+ name: 'Some Policy Name',
+ package: {
+ name: 'endpoint',
+ title: '',
+ version: '',
+ },
+ id: 'endpoint',
+ policy_id: '',
+ enabled: true,
+ namespace: '',
+ output_id: '',
+ revision: 0,
+ updated_at: '',
+ updated_by: '',
+ created_at: '',
+ created_by: '',
+ inputs: [
+ {
+ type: 'endpoint',
+ enabled: true,
+ streams: [],
+ config: {
+ policy: {
+ value: {
+ windows: {},
+ mac: {},
+ },
+ },
+ },
+ },
+ ],
+ },
+ type: ' nested',
+ };
+
+ expect(
+ migration(doc, {} as SavedObjectMigrationContext) as SavedObjectUnsanitizedDoc
+ ).toEqual({
+ attributes: {
+ name: 'Some Policy Name',
+ package: {
+ name: 'endpoint',
+ title: '',
+ version: '',
+ },
+ id: 'endpoint',
+ policy_id: '',
+ enabled: true,
+ namespace: '',
+ output_id: '',
+ revision: 0,
+ updated_at: '',
+ updated_by: '',
+ created_at: '',
+ created_by: '',
+ inputs: [
+ {
+ type: 'endpoint',
+ enabled: true,
+ streams: [],
+ config: {
+ policy: {
+ value: {
+ windows: {
+ popup: {
+ malware: {
+ message: '',
+ enabled: false,
+ },
+ },
+ },
+ mac: {
+ popup: {
+ malware: {
+ message: '',
+ enabled: false,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ type: ' nested',
+ });
+ });
+
+ it('does not modify non-endpoint package policies', () => {
+ const doc: SavedObjectUnsanitizedDoc = {
+ attributes: {
+ name: 'Some Policy Name',
+ package: {
+ name: 'notEndpoint',
+ title: '',
+ version: '',
+ },
+ id: 'notEndpoint',
+ policy_id: '',
+ enabled: true,
+ namespace: '',
+ output_id: '',
+ revision: 0,
+ updated_at: '',
+ updated_by: '',
+ created_at: '',
+ created_by: '',
+ inputs: [
+ {
+ type: 'notEndpoint',
+ enabled: true,
+ streams: [],
+ config: {},
+ },
+ ],
+ },
+ type: ' nested',
+ };
+
+ expect(
+ migration(doc, {} as SavedObjectMigrationContext) as SavedObjectUnsanitizedDoc
+ ).toEqual({
+ attributes: {
+ name: 'Some Policy Name',
+ package: {
+ name: 'notEndpoint',
+ title: '',
+ version: '',
+ },
+ id: 'notEndpoint',
+ policy_id: '',
+ enabled: true,
+ namespace: '',
+ output_id: '',
+ revision: 0,
+ updated_at: '',
+ updated_by: '',
+ created_at: '',
+ created_by: '',
+ inputs: [
+ {
+ type: 'notEndpoint',
+ enabled: true,
+ streams: [],
+ config: {},
+ },
+ ],
+ },
+ type: ' nested',
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts
new file mode 100644
index 00000000000000..8c2dabae21bbde
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server';
+import { cloneDeep } from 'lodash';
+import { PackagePolicy } from '../../../../../ingest_manager/common';
+
+export const migratePackagePolicyToV7110: SavedObjectMigrationFn = (
+ packagePolicyDoc
+) => {
+ const updatedPackagePolicyDoc: SavedObjectUnsanitizedDoc = cloneDeep(
+ packagePolicyDoc
+ );
+ if (packagePolicyDoc.attributes.package?.name === 'endpoint') {
+ const input = updatedPackagePolicyDoc.attributes.inputs[0];
+ const popup = {
+ malware: {
+ message: '',
+ enabled: false,
+ },
+ };
+ if (input && input.config) {
+ input.config.policy.value.windows.popup = popup;
+ input.config.policy.value.mac.popup = popup;
+ }
+ }
+
+ return updatedPackagePolicyDoc;
+};
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
index f2033e064ef721..882b3e5182bf39 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
@@ -873,6 +873,12 @@ export interface PolicyConfig {
logging: {
file: string;
};
+ popup: {
+ malware: {
+ message: string;
+ enabled: boolean;
+ };
+ };
};
mac: {
events: {
@@ -881,6 +887,12 @@ export interface PolicyConfig {
network: boolean;
};
malware: MalwareFields;
+ popup: {
+ malware: {
+ message: string;
+ enabled: boolean;
+ };
+ };
logging: {
file: string;
};
@@ -904,11 +916,11 @@ export interface UIPolicyConfig {
/**
* Windows-specific policy configuration that is supported via the UI
*/
- windows: Pick;
+ windows: Pick;
/**
* Mac-specific policy configuration that is supported via the UI
*/
- mac: Pick;
+ mac: Pick;
/**
* Linux-specific policy configuration that is supported via the UI
*/
diff --git a/x-pack/plugins/security_solution/common/shared_exports.ts b/x-pack/plugins/security_solution/common/shared_exports.ts
index 6269c3cee999c9..bee2e54d0e3eab 100644
--- a/x-pack/plugins/security_solution/common/shared_exports.ts
+++ b/x-pack/plugins/security_solution/common/shared_exports.ts
@@ -16,3 +16,4 @@ export { exactCheck } from './exact_check';
export { getPaths, foldLeftRight } from './test_utils';
export { validate, validateEither } from './validate';
export { formatErrors } from './format_errors';
+export { migratePackagePolicyToV7110 } from './endpoint/policy/migrations/to_v7_11.0';
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
index 41665cf6d20a44..491f4f8952fd98 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
@@ -73,6 +73,7 @@ import {
import {
changeToThreeHundredRowsPerPage,
deleteFirstRule,
+ deleteRule,
deleteSelectedRules,
editFirstRule,
filterByCustomRules,
@@ -91,12 +92,12 @@ import {
goToAboutStepTab,
goToActionsStepTab,
goToScheduleStepTab,
+ waitForAlertsToPopulate,
waitForTheRuleToBeExecuted,
} from '../tasks/create_new_rule';
import { saveEditedRule, waitForKibana } from '../tasks/edit_rule';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
-import { refreshPage } from '../tasks/security_header';
import { DETECTIONS_URL } from '../urls/navigation';
@@ -119,6 +120,7 @@ describe('Custom detection rules creation', () => {
});
after(() => {
+ deleteRule();
esArchiverUnload('timeline');
});
@@ -197,14 +199,10 @@ describe('Custom detection rules creation', () => {
);
});
- refreshPage();
waitForTheRuleToBeExecuted();
+ waitForAlertsToPopulate();
- cy.get(NUMBER_OF_ALERTS)
- .invoke('text')
- .then((numberOfAlertsText) => {
- cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.above', 0);
- });
+ cy.get(NUMBER_OF_ALERTS).invoke('text').then(parseFloat).should('be.above', 0);
cy.get(ALERT_RULE_NAME).first().should('have.text', newRule.name);
cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query');
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts
index 5502f35d6f0f84..bee4713ca7cda1 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts
@@ -55,6 +55,7 @@ import {
} from '../tasks/alerts';
import {
changeToThreeHundredRowsPerPage,
+ deleteRule,
filterByCustomRules,
goToCreateNewRule,
goToRuleDetails,
@@ -67,11 +68,11 @@ import {
fillDefineEqlRuleAndContinue,
fillScheduleRuleAndContinue,
selectEqlRuleType,
+ waitForAlertsToPopulate,
waitForTheRuleToBeExecuted,
} from '../tasks/create_new_rule';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
-import { refreshPage } from '../tasks/security_header';
import { DETECTIONS_URL } from '../urls/navigation';
@@ -87,13 +88,13 @@ const expectedNumberOfRules = 1;
const expectedNumberOfAlerts = 7;
const expectedNumberOfSequenceAlerts = 1;
-// Failing: See https://github.com/elastic/kibana/issues/79522
-describe.skip('Detection rules, EQL', () => {
+describe('Detection rules, EQL', () => {
beforeEach(() => {
esArchiverLoad('timeline');
});
afterEach(() => {
+ deleteRule();
esArchiverUnload('timeline');
});
@@ -160,14 +161,10 @@ describe.skip('Detection rules, EQL', () => {
);
});
- refreshPage();
waitForTheRuleToBeExecuted();
+ waitForAlertsToPopulate();
- cy.get(NUMBER_OF_ALERTS)
- .invoke('text')
- .then((numberOfAlertsText) => {
- cy.wrap(parseInt(numberOfAlertsText, 10)).should('eql', expectedNumberOfAlerts);
- });
+ cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts);
cy.get(ALERT_RULE_NAME).first().should('have.text', eqlRule.name);
cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
cy.get(ALERT_RULE_METHOD).first().should('have.text', 'eql');
@@ -199,14 +196,10 @@ describe.skip('Detection rules, EQL', () => {
filterByCustomRules();
goToRuleDetails();
- refreshPage();
waitForTheRuleToBeExecuted();
+ waitForAlertsToPopulate();
- cy.get(NUMBER_OF_ALERTS)
- .invoke('text')
- .then((numberOfAlertsText) => {
- cy.wrap(parseInt(numberOfAlertsText, 10)).should('eql', expectedNumberOfSequenceAlerts);
- });
+ cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfSequenceAlerts);
cy.get(ALERT_RULE_NAME).first().should('have.text', eqlSequenceRule.name);
cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
cy.get(ALERT_RULE_METHOD).first().should('have.text', 'eql');
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts
index 0f34e7d71e5faa..153c55fae59fe6 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts
@@ -44,6 +44,7 @@ import {
} from '../tasks/alerts';
import {
changeToThreeHundredRowsPerPage,
+ deleteRule,
filterByCustomRules,
goToCreateNewRule,
goToRuleDetails,
@@ -78,6 +79,7 @@ describe('Detection rules, machine learning', () => {
});
after(() => {
+ deleteRule();
esArchiverUnload('prebuilt_rules_loaded');
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts
index edf7305f6916e6..e31fe2e9a39113 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts
@@ -72,11 +72,11 @@ import {
fillAboutRuleWithOverrideAndContinue,
fillDefineCustomRuleWithImportedQueryAndContinue,
fillScheduleRuleAndContinue,
+ waitForAlertsToPopulate,
waitForTheRuleToBeExecuted,
} from '../tasks/create_new_rule';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
-import { refreshPage } from '../tasks/security_header';
import { DETECTIONS_URL } from '../urls/navigation';
@@ -179,14 +179,10 @@ describe('Detection rules, override', () => {
);
});
- refreshPage();
waitForTheRuleToBeExecuted();
+ waitForAlertsToPopulate();
- cy.get(NUMBER_OF_ALERTS)
- .invoke('text')
- .then((numberOfAlertsText) => {
- cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.above', 0);
- });
+ cy.get(NUMBER_OF_ALERTS).invoke('text').then(parseFloat).should('be.above', 0);
cy.get(ALERT_RULE_NAME).first().should('have.text', 'auditbeat');
cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query');
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts
index 5095e856e3f65d..a6f974256f3e49 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts
@@ -56,6 +56,7 @@ import {
} from '../tasks/alerts';
import {
changeToThreeHundredRowsPerPage,
+ deleteRule,
filterByCustomRules,
goToCreateNewRule,
goToRuleDetails,
@@ -68,11 +69,11 @@ import {
fillDefineThresholdRuleAndContinue,
fillScheduleRuleAndContinue,
selectThresholdRuleType,
+ waitForAlertsToPopulate,
waitForTheRuleToBeExecuted,
} from '../tasks/create_new_rule';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
-import { refreshPage } from '../tasks/security_header';
import { DETECTIONS_URL } from '../urls/navigation';
@@ -91,6 +92,7 @@ describe('Detection rules, threshold', () => {
});
after(() => {
+ deleteRule();
esArchiverUnload('timeline');
});
@@ -162,14 +164,10 @@ describe('Detection rules, threshold', () => {
);
});
- refreshPage();
waitForTheRuleToBeExecuted();
+ waitForAlertsToPopulate();
- cy.get(NUMBER_OF_ALERTS)
- .invoke('text')
- .then((numberOfAlertsText) => {
- cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.below', 100);
- });
+ cy.get(NUMBER_OF_ALERTS).invoke('text').then(parseFloat).should('be.below', 100);
cy.get(ALERT_RULE_NAME).first().should('have.text', newThresholdRule.name);
cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold');
diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts
index e1ab5ff30572f8..5e7dce6966195d 100644
--- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts
@@ -31,7 +31,11 @@ export const COMBO_BOX_INPUT = '[data-test-subj="comboBoxInput"]';
export const CREATE_AND_ACTIVATE_BTN = '[data-test-subj="create-activate"]';
-export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]';
+export const CUSTOM_QUERY_INPUT =
+ '[data-test-subj="detectionEngineStepDefineRuleQueryBar"] [data-test-subj="queryInput"]';
+
+export const THREAT_MATCH_QUERY_INPUT =
+ '[data-test-subj="detectionEngineStepDefineThreatRuleQueryBar"] [data-test-subj="queryInput"]';
export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]';
diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts
index 5a376e95e38dd3..e40b81ed0e8567 100644
--- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts
@@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export const ALL_ACTIONS = '[data-test-subj="rules-details-popover-button-icon"]';
+
export const ABOUT_INVESTIGATION_NOTES = '[data-test-subj="stepAboutDetailsNoteContent"]';
export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]';
@@ -24,6 +26,8 @@ export const DETAILS_DESCRIPTION = '.euiDescriptionList__description';
export const DETAILS_TITLE = '.euiDescriptionList__title';
+export const DELETE_RULE = '[data-test-subj=rules-details-delete-rule]';
+
export const FALSE_POSITIVES_DETAILS = 'False positive examples';
export const INDEX_PATTERNS_DETAILS = 'Index patterns';
diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js
index 0dbcb1af4642f6..dbd60cdd31a5a6 100644
--- a/x-pack/plugins/security_solution/cypress/support/commands.js
+++ b/x-pack/plugins/security_solution/cypress/support/commands.js
@@ -69,3 +69,36 @@ Cypress.Commands.add(
});
}
);
+
+const waitUntil = (subject, fn, options = {}) => {
+ const { interval = 200, timeout = 5000 } = options;
+ let attempts = Math.floor(timeout / interval);
+
+ const completeOrRetry = (result) => {
+ if (result) {
+ return result;
+ }
+ if (attempts < 1) {
+ throw new Error(`Timed out while retrying, last result was: {${result}}`);
+ }
+ cy.wait(interval, { log: false }).then(() => {
+ attempts--;
+ // eslint-disable-next-line no-use-before-define
+ return evaluate();
+ });
+ };
+
+ const evaluate = () => {
+ const result = fn(subject);
+
+ if (result && result.then) {
+ return result.then(completeOrRetry);
+ } else {
+ return completeOrRetry(result);
+ }
+ };
+
+ return evaluate();
+};
+
+Cypress.Commands.add('waitUntil', { prevSubject: 'optional' }, waitUntil);
diff --git a/x-pack/plugins/security_solution/cypress/support/index.d.ts b/x-pack/plugins/security_solution/cypress/support/index.d.ts
index 59180507cbadee..0cf3cf614cdb9e 100644
--- a/x-pack/plugins/security_solution/cypress/support/index.d.ts
+++ b/x-pack/plugins/security_solution/cypress/support/index.d.ts
@@ -14,5 +14,12 @@ declare namespace Cypress {
searchStrategyName?: string
): Chainable;
attachFile(fileName: string, fileType?: string): Chainable;
+ waitUntil(
+ fn: (subject: Subject) => boolean | Chainable,
+ options?: {
+ interval: number;
+ timeout: number;
+ }
+ ): Chainable;
}
}
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 8b494edaade3a8..1c430e12b6b734 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
@@ -27,6 +27,7 @@ import {
EDIT_RULE_ACTION_BTN,
NEXT_BTN,
} from '../screens/alerts_detection_rules';
+import { ALL_ACTIONS, DELETE_RULE } from '../screens/rule_details';
export const activateRule = (rulePosition: number) => {
cy.get(RULE_SWITCH).eq(rulePosition).click({ force: true });
@@ -47,6 +48,11 @@ export const deleteFirstRule = () => {
cy.get(DELETE_RULE_ACTION_BTN).click();
};
+export const deleteRule = () => {
+ cy.get(ALL_ACTIONS).click();
+ cy.get(DELETE_RULE).click();
+};
+
export const deleteSelectedRules = () => {
cy.get(BULK_ACTIONS_BTN).click({ force: true });
cy.get(DELETE_RULE_BULK_BTN).click();
diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
index fa3c219595c729..5b2c365dfd8c34 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
@@ -11,6 +11,7 @@ import {
OverrideRule,
ThresholdRule,
} from '../objects/rule';
+import { NUMBER_OF_ALERTS } from '../screens/alerts';
import {
ABOUT_CONTINUE_BTN,
ABOUT_EDIT_TAB,
@@ -62,6 +63,7 @@ import {
EQL_QUERY_INPUT,
} from '../screens/create_new_rule';
import { TIMELINE } from '../screens/timelines';
+import { refreshPage } from './security_header';
export const createAndActivateRule = () => {
cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true });
@@ -263,12 +265,27 @@ export const selectThresholdRuleType = () => {
cy.get(THRESHOLD_TYPE).click({ force: true });
};
-export const waitForTheRuleToBeExecuted = async () => {
- let status = '';
- while (status !== 'succeeded') {
+export const waitForTheRuleToBeExecuted = () => {
+ cy.waitUntil(() => {
cy.get(REFRESH_BUTTON).click();
- status = await cy.get(RULE_STATUS).invoke('text').promisify();
- }
+ return cy
+ .get(RULE_STATUS)
+ .invoke('text')
+ .then((ruleStatus) => ruleStatus === 'succeeded');
+ });
+};
+
+export const waitForAlertsToPopulate = async () => {
+ cy.waitUntil(() => {
+ refreshPage();
+ return cy
+ .get(NUMBER_OF_ALERTS)
+ .invoke('text')
+ .then((countText) => {
+ const alertCount = parseInt(countText, 10) || 0;
+ return alertCount > 0;
+ });
+ });
};
export const selectEqlRuleType = () => {
diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_header.ts b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts
index 28efc47120d326..a28767b489e46d 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/security_header.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts
@@ -19,9 +19,9 @@ export const navigateFromHeaderTo = (page: string) => {
};
export const refreshPage = () => {
- cy.get(REFRESH_BUTTON).click({ force: true }).invoke('text').should('not.equal', 'Updating');
+ cy.get(REFRESH_BUTTON).click({ force: true }).should('not.have.text', 'Updating');
};
export const waitForThePageToBeUpdated = () => {
- cy.get(REFRESH_BUTTON).should('not.equal', 'Updating');
+ cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating');
};
diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json
index 1fea4bb1aba54a..145e34c4fc99ca 100644
--- a/x-pack/plugins/security_solution/kibana.json
+++ b/x-pack/plugins/security_solution/kibana.json
@@ -28,7 +28,8 @@
"usageCollection",
"lists",
"home",
- "telemetry"
+ "telemetry",
+ "telemetryManagementSection"
],
"server": true,
"ui": true,
diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
index 3e3d21b9926d14..5b77c4d99a9517 100644
--- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
@@ -54,7 +54,7 @@ export const AddComment = React.memo(
const fieldName = 'comment';
const { setFieldValue, reset, submit } = form;
- const [{ comment }] = useFormData({ form, watch: [fieldName] });
+ const [{ comment }] = useFormData<{ comment: string }>({ form, watch: [fieldName] });
const onCommentChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [
setFieldValue,
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
index 45e46b2d7d2db8..5bffebbefa40fc 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
@@ -4,24 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonEmpty,
- EuiButton,
- EuiMarkdownFormat,
-} from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import * as i18n from '../case_view/translations';
import { Form, useForm, UseField } from '../../../shared_imports';
import { schema, Content } from './schema';
-import {
- MarkdownEditorForm,
- parsingPlugins,
- processingPlugins,
-} from '../../../common/components/markdown_editor/eui_form';
+import { MarkdownRenderer, MarkdownEditorForm } from '../../../common/components/markdown_editor';
const ContentWrapper = styled.div`
padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`};
@@ -111,12 +101,7 @@ export const UserActionMarkdown = ({
) : (
-
- {content}
-
+ {content}
);
};
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
index 152161e2ce3a43..60e7accf494587 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
@@ -104,6 +104,7 @@ interface Props {
kqlMode: KqlMode;
onChangeItemsPerPage: OnChangeItemsPerPage;
query: Query;
+ onRuleChange?: () => void;
start: string;
sort: Sort;
toggleColumn: (column: ColumnHeaderOptions) => void;
@@ -131,6 +132,7 @@ const EventsViewerComponent: React.FC = ({
kqlMode,
onChangeItemsPerPage,
query,
+ onRuleChange,
start,
sort,
toggleColumn,
@@ -286,6 +288,7 @@ const EventsViewerComponent: React.FC = ({
docValueFields={docValueFields}
id={id}
isEventViewer={true}
+ onRuleChange={onRuleChange}
refetch={refetch}
sort={sort}
toggleColumn={toggleColumn}
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
index c53d311dc1361c..a4f2b0536abf5c 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
@@ -42,6 +42,7 @@ export interface OwnProps {
start: string;
headerFilterGroup?: React.ReactNode;
pageFilters?: Filter[];
+ onRuleChange?: () => void;
utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode;
}
@@ -64,6 +65,7 @@ const StatefulEventsViewerComponent: React.FC = ({
kqlMode,
pageFilters,
query,
+ onRuleChange,
removeColumn,
start,
scopeId,
@@ -153,6 +155,7 @@ const StatefulEventsViewerComponent: React.FC = ({
kqlMode={kqlMode}
onChangeItemsPerPage={onChangeItemsPerPage}
query={query}
+ onRuleChange={onRuleChange}
start={start}
sort={sort}
toggleColumn={toggleColumn}
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx
index c97895cdfe2363..f90c83bf953ea4 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx
@@ -18,7 +18,7 @@ import { ExceptionListItemIdentifiers, Filter } from '../types';
import { allExceptionItemsReducer, State, ViewerModalName } from './reducer';
import {
useExceptionList,
- ExceptionIdentifiers,
+ ExceptionListIdentifiers,
ExceptionListTypeEnum,
ExceptionListItemSchema,
UseExceptionListSuccess,
@@ -54,7 +54,7 @@ interface ExceptionsViewerProps {
ruleId: string;
ruleName: string;
ruleIndices: string[];
- exceptionListsMeta: ExceptionIdentifiers[];
+ exceptionListsMeta: ExceptionListIdentifiers[];
availableListTypes: ExceptionListTypeEnum[];
commentsAccordionId: string;
onRuleChange?: () => void;
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts
index ca7471b0f82396..9c2a89829b59e7 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts
@@ -12,7 +12,7 @@ import {
import {
ExceptionListType,
ExceptionListItemSchema,
- ExceptionIdentifiers,
+ ExceptionListIdentifiers,
Pagination,
} from '../../../../../public/lists_plugin_deps';
@@ -36,7 +36,7 @@ export interface State {
export type Action =
| {
type: 'setExceptions';
- lists: ExceptionIdentifiers[];
+ lists: ExceptionListIdentifiers[];
exceptions: ExceptionListItemSchema[];
pagination: Pagination;
}
@@ -48,7 +48,7 @@ export type Action =
| { type: 'updateModalOpen'; modalName: ViewerModalName }
| {
type: 'updateExceptionToEdit';
- lists: ExceptionIdentifiers[];
+ lists: ExceptionListIdentifiers[];
exception: ExceptionListItemSchema;
}
| { type: 'updateLoadingItemIds'; items: ExceptionListItemIdentifiers[] }
diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap
index 80144311921700..d95e0300fe140e 100644
--- a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap
@@ -1,21 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`item_details_card ItemDetailsAction should render correctly 1`] = `
-
+
primary
-
+
`;
exports[`item_details_card ItemDetailsCard should render correctly with actions 1`] = `
diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx
index 37003961d67d04..829d8db5a5a0f0 100644
--- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx
@@ -80,18 +80,12 @@ export const ItemDetailsPropertySummary = memo(
ItemDetailsPropertySummary.displayName = 'ItemPropertySummary';
export const ItemDetailsAction: FC> = memo(
- ({ children, ...rest }) => (
- <>
-
+ ({ children, className = '', ...rest }) => (
+
+
{children}
- >
+
)
);
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/index.test.tsx.snap
deleted file mode 100644
index 4850547f30c527..00000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/index.test.tsx.snap
+++ /dev/null
@@ -1,58 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Markdown markdown links it renders the expected content containing a link 1`] = `
-
-`;
-
-exports[`Markdown markdown tables it renders the expected table content 1`] = `
-
-`;
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/markdown_hint.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/markdown_hint.test.tsx.snap
deleted file mode 100644
index 7f350072439c52..00000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/markdown_hint.test.tsx.snap
+++ /dev/null
@@ -1,55 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MarkdownHintComponent rendering it renders the expected hints 1`] = `
-
-
- # heading
-
-
- **bold**
-
-
- _italics_
-
-
- \`code\`
-
-
- [link](url)
-
-
- * bullet
-
-
- \`\`\`preformatted\`\`\`
-
-
- >quote
-
- ~~
-
- strikethrough
-
- ~~
-
- ![image](url)
-
-
-`;
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown/index.test.tsx
deleted file mode 100644
index e30391982ee7a0..00000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/index.test.tsx
+++ /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;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { mount, shallow } from 'enzyme';
-import React from 'react';
-
-import { Markdown } from '.';
-
-describe('Markdown', () => {
- test(`it renders when raw markdown is NOT provided`, () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="markdown"]').exists()).toEqual(true);
- });
-
- test('it renders plain text', () => {
- const raw = 'this has no special markdown formatting';
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="markdown-root"]').first().text()).toEqual(raw);
- });
-
- test('it applies the EUI text style to all markdown content', () => {
- const wrapper = mount( );
-
- expect(
- wrapper.find('[data-test-subj="markdown-root"]').first().childAt(0).hasClass('euiText')
- ).toBe(true);
- });
-
- describe('markdown tables', () => {
- const headerColumns = ['we', 'support', 'markdown', 'tables'];
- const header = `| ${headerColumns[0]} | ${headerColumns[1]} | ${headerColumns[2]} | ${headerColumns[3]} |`;
-
- const rawTable = `${header}\n|---------|---------|------------|--------|\n| because | tables | are | pretty |\n| useful | for | formatting | data |`;
-
- test('it applies EUI table styling to tables', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('table').first().childAt(0).hasClass('euiTable')).toBe(true);
- });
-
- headerColumns.forEach((headerText) => {
- test(`it renders the "${headerText}" table header`, () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="markdown-table-header"]').first().text()).toContain(
- headerText
- );
- });
- });
-
- test('it applies EUI table styling to table rows', () => {
- const wrapper = mount( );
-
- expect(
- wrapper
- .find('[data-test-subj="markdown-table-row"]')
- .first()
- .childAt(0)
- .hasClass('euiTableRow')
- ).toBe(true);
- });
-
- test('it applies EUI table styling to table cells', () => {
- const wrapper = mount( );
-
- expect(
- wrapper
- .find('[data-test-subj="markdown-table-cell"]')
- .first()
- .childAt(0)
- .hasClass('euiTableRowCell')
- ).toBe(true);
- });
-
- test('it renders the expected table content', () => {
- const wrapper = shallow( );
-
- expect(wrapper).toMatchSnapshot();
- });
- });
-
- describe('markdown links', () => {
- const markdownWithLink = 'A link to an external site [External Site](https://google.com)';
-
- test('it renders the expected link text', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="markdown-link"]').first().text()).toEqual(
- 'External Site'
- );
- });
-
- test('it renders the expected href', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
- 'href',
- 'https://google.com/'
- );
- });
-
- test('it does NOT render the href if links are disabled', () => {
- const wrapper = mount( );
-
- expect(
- wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()
- ).not.toHaveProperty('href');
- });
-
- test('it opens links in a new tab via target="_blank"', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
- 'target',
- '_blank'
- );
- });
-
- test('it sets the link `rel` attribute to `noopener` to prevent the new page from accessing `window.opener`, `nofollow` to note the link is not endorsed by us, and noreferrer to prevent the browser from sending the current address', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
- 'rel',
- 'nofollow noopener noreferrer'
- );
- });
-
- test('it renders the expected content containing a link', () => {
- const wrapper = shallow( );
-
- expect(wrapper).toMatchSnapshot();
- });
-
- describe('markdown timeline links', () => {
- const timelineId = '1e10f150-949b-11ea-b63c-2bc51864784c';
- const markdownWithTimelineLink = `A link to a timeline [timeline](http://localhost:5601/app/siem#/timelines?timeline=(id:'${timelineId}',isOpen:!t))`;
- const onClickTimeline = jest.fn();
- beforeEach(() => {
- jest.resetAllMocks();
- });
- test('it renders a timeline link without href when provided the onClickTimeline argument', () => {
- const wrapper = mount(
-
- );
-
- expect(
- wrapper.find('[data-test-subj="markdown-timeline-link"]').first().getDOMNode()
- ).not.toHaveProperty('href');
- });
- test('timeline link onClick calls onClickTimeline with timelineId', () => {
- const wrapper = mount(
-
- );
- wrapper.find('[data-test-subj="markdown-timeline-link"]').first().simulate('click');
-
- expect(onClickTimeline).toHaveBeenCalledWith(timelineId, '');
- });
-
- test('timeline link onClick calls onClickTimeline with timelineId and graphEventId', () => {
- const graphEventId = '2bc51864784c';
- const markdownWithTimelineAndGraphEventLink = `A link to a timeline [timeline](http://localhost:5601/app/siem#/timelines?timeline=(id:'${timelineId}',isOpen:!t,graphEventId:'${graphEventId}'))`;
-
- const wrapper = mount(
-
- );
- wrapper.find('[data-test-subj="markdown-timeline-link"]').first().simulate('click');
-
- expect(onClickTimeline).toHaveBeenCalledWith(timelineId, graphEventId);
- });
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown/index.tsx
deleted file mode 100644
index 1d73c3cb8a2aa5..00000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/index.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-/* eslint-disable react/display-name */
-
-import { EuiLink, EuiTableRow, EuiTableRowCell, EuiText, EuiToolTip } from '@elastic/eui';
-import { clone } from 'lodash/fp';
-import React from 'react';
-import ReactMarkdown from 'react-markdown';
-import styled, { css } from 'styled-components';
-import * as i18n from './translations';
-
-const TableHeader = styled.thead`
- font-weight: bold;
-`;
-
-const MyBlockquote = styled.div`
- ${({ theme }) => css`
- padding: 0 ${theme.eui.euiSize};
- color: ${theme.eui.euiColorMediumShade};
- border-left: ${theme.eui.euiSizeXS} solid ${theme.eui.euiColorLightShade};
- `}
-`;
-
-TableHeader.displayName = 'TableHeader';
-
-/** prevents links to the new pages from accessing `window.opener` */
-const REL_NOOPENER = 'noopener';
-
-/** prevents search engine manipulation by noting the linked document is not trusted or endorsed by us */
-const REL_NOFOLLOW = 'nofollow';
-
-/** prevents the browser from sending the current address as referrer via the Referer HTTP header */
-const REL_NOREFERRER = 'noreferrer';
-
-export const Markdown = React.memo<{
- disableLinks?: boolean;
- raw?: string;
- onClickTimeline?: (timelineId: string, graphEventId?: string) => void;
- size?: 'xs' | 's' | 'm';
-}>(({ disableLinks = false, onClickTimeline, raw, size = 's' }) => {
- const markdownRenderers = {
- root: ({ children }: { children: React.ReactNode[] }) => (
-
- {children}
-
- ),
- table: ({ children }: { children: React.ReactNode[] }) => (
-
- ),
- tableHead: ({ children }: { children: React.ReactNode[] }) => (
- {children}
- ),
- tableRow: ({ children }: { children: React.ReactNode[] }) => (
- {children}
- ),
- tableCell: ({ children }: { children: React.ReactNode[] }) => (
- {children}
- ),
- link: ({ children, href }: { children: React.ReactNode[]; href?: string }) => {
- if (onClickTimeline != null && href != null && href.indexOf(`timelines?timeline=(id:`) > -1) {
- const timelineId = clone(href).split('timeline=(id:')[1].split("'")[1] ?? '';
- const graphEventId = href.includes('graphEventId:')
- ? clone(href).split('graphEventId:')[1].split("'")[1] ?? ''
- : '';
- return (
-
- onClickTimeline(timelineId, graphEventId)}
- data-test-subj="markdown-timeline-link"
- >
- {children}
-
-
- );
- }
- return (
-
-
- {children}
-
-
- );
- },
- blockquote: ({ children }: { children: React.ReactNode[] }) => (
- {children}
- ),
- };
-
- return (
-
- );
-});
-
-Markdown.displayName = 'Markdown';
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.test.tsx
deleted file mode 100644
index 5ec37f8aed0cbd..00000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.test.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { mount, shallow } from 'enzyme';
-import React from 'react';
-
-import { MarkdownHintComponent } from './markdown_hint';
-
-describe('MarkdownHintComponent ', () => {
- test('it has inline visibility when show is true', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
- 'visibility',
- 'inline'
- );
- });
-
- test('it has hidden visibility when show is false', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
- 'visibility',
- 'hidden'
- );
- });
-
- test('it renders the heading hint', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="heading-hint"]').first().text()).toEqual('# heading');
- });
-
- test('it renders the bold hint with a bold font-weight', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="bold-hint"]').first()).toHaveStyleRule(
- 'font-weight',
- 'bold'
- );
- });
-
- test('it renders the italic hint with an italic font-style', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="italic-hint"]').first()).toHaveStyleRule(
- 'font-style',
- 'italic'
- );
- });
-
- test('it renders the code hint with a monospace font family', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="code-hint"]').first()).toHaveStyleRule(
- 'font-family',
- 'monospace'
- );
- });
-
- test('it renders the preformatted hint with a monospace font family', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="preformatted-hint"]').first()).toHaveStyleRule(
- 'font-family',
- 'monospace'
- );
- });
-
- test('it renders the strikethrough hint with a line-through text-decoration', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="strikethrough-hint"]').first()).toHaveStyleRule(
- 'text-decoration',
- 'line-through'
- );
- });
-
- describe('rendering', () => {
- test('it renders the expected hints', () => {
- const wrapper = shallow( );
-
- expect(wrapper).toMatchSnapshot();
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.tsx b/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.tsx
deleted file mode 100644
index 199059670e4bd2..00000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { EuiText } from '@elastic/eui';
-import React from 'react';
-import styled from 'styled-components';
-
-import * as i18n from './translations';
-
-const Heading = styled.span`
- margin-right: 5px;
-`;
-
-Heading.displayName = 'Heading';
-
-const Bold = styled.span`
- font-weight: bold;
- margin-right: 5px;
-`;
-
-Bold.displayName = 'Bold';
-
-const MarkdownHintContainer = styled(EuiText)<{ visibility: string }>`
- visibility: ${({ visibility }) => visibility};
-`;
-
-MarkdownHintContainer.displayName = 'MarkdownHintContainer';
-
-const ImageUrl = styled.span`
- margin-left: 5px;
-`;
-
-ImageUrl.displayName = 'ImageUrl';
-
-const Italic = styled.span`
- font-style: italic;
- margin-right: 5px;
-`;
-
-Italic.displayName = 'Italic';
-
-const Strikethrough = styled.span`
- text-decoration: line-through;
-`;
-
-Strikethrough.displayName = 'Strikethrough';
-
-const Code = styled.span`
- font-family: monospace;
- margin-right: 5px;
-`;
-
-Code.displayName = 'Code';
-
-const TrailingWhitespace = styled.span`
- margin-right: 5px;
-`;
-
-TrailingWhitespace.displayName = 'TrailingWhitespace';
-
-export const MarkdownHintComponent = ({ show }: { show: boolean }) => (
-
- {i18n.MARKDOWN_HINT_HEADING}
- {i18n.MARKDOWN_HINT_BOLD}
- {i18n.MARKDOWN_HINT_ITALICS}
- {i18n.MARKDOWN_HINT_CODE}
- {i18n.MARKDOWN_HINT_URL}
- {i18n.MARKDOWN_HINT_BULLET}
- {i18n.MARKDOWN_HINT_PREFORMATTED}
- {i18n.MARKDOWN_HINT_QUOTE}
- {'~~'}
-
- {i18n.MARKDOWN_HINT_STRIKETHROUGH}
-
- {'~~'}
- {i18n.MARKDOWN_HINT_IMAGE_URL}
-
-);
-
-MarkdownHintComponent.displayName = 'MarkdownHintComponent';
-
-export const MarkdownHint = React.memo(MarkdownHintComponent);
-
-MarkdownHint.displayName = 'MarkdownHint';
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/translations.ts b/x-pack/plugins/security_solution/public/common/components/markdown/translations.ts
deleted file mode 100644
index 98d2e7d47b3fbc..00000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/translations.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { i18n } from '@kbn/i18n';
-
-export const MARKDOWN_HINT_HEADING = i18n.translate(
- 'xpack.securitySolution.markdown.hint.headingLabel',
- {
- defaultMessage: '# heading',
- }
-);
-
-export const MARKDOWN_HINT_BOLD = i18n.translate('xpack.securitySolution.markdown.hint.boldLabel', {
- defaultMessage: '**bold**',
-});
-
-export const MARKDOWN_HINT_ITALICS = i18n.translate(
- 'xpack.securitySolution.markdown.hint.italicsLabel',
- {
- defaultMessage: '_italics_',
- }
-);
-
-export const MARKDOWN_HINT_CODE = i18n.translate('xpack.securitySolution.markdown.hint.codeLabel', {
- defaultMessage: '`code`',
-});
-
-export const MARKDOWN_HINT_URL = i18n.translate('xpack.securitySolution.markdown.hint.urlLabel', {
- defaultMessage: '[link](url)',
-});
-
-export const MARKDOWN_HINT_BULLET = i18n.translate(
- 'xpack.securitySolution.markdown.hint.bulletLabel',
- {
- defaultMessage: '* bullet',
- }
-);
-
-export const MARKDOWN_HINT_PREFORMATTED = i18n.translate(
- 'xpack.securitySolution.markdown.hint.preformattedLabel',
- {
- defaultMessage: '```preformatted```',
- }
-);
-
-export const MARKDOWN_HINT_QUOTE = i18n.translate(
- 'xpack.securitySolution.markdown.hint.quoteLabel',
- {
- defaultMessage: '>quote',
- }
-);
-
-export const MARKDOWN_HINT_STRIKETHROUGH = i18n.translate(
- 'xpack.securitySolution.markdown.hint.strikethroughLabel',
- {
- defaultMessage: 'strikethrough',
- }
-);
-
-export const MARKDOWN_HINT_IMAGE_URL = i18n.translate(
- 'xpack.securitySolution.markdown.hint.imageUrlLabel',
- {
- defaultMessage: '![image](url)',
- }
-);
-
-export const TIMELINE_ID = (timelineId: string) =>
- i18n.translate('xpack.securitySolution.markdown.toolTip.timelineId', {
- defaultMessage: 'Timeline id: { timelineId }',
- values: {
- timelineId,
- },
- });
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx
new file mode 100644
index 00000000000000..b8632de71abe12
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo, useState, useCallback } from 'react';
+import { EuiMarkdownEditor } from '@elastic/eui';
+
+import { uiPlugins, parsingPlugins, processingPlugins } from './plugins';
+
+interface MarkdownEditorProps {
+ onChange: (content: string) => void;
+ value: string;
+ ariaLabel: string;
+ editorId?: string;
+ dataTestSubj?: string;
+ height?: number;
+}
+
+const MarkdownEditorComponent: React.FC = ({
+ onChange,
+ value,
+ ariaLabel,
+ editorId,
+ dataTestSubj,
+ height,
+}) => {
+ const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]);
+ const onParse = useCallback((err, { messages }) => {
+ setMarkdownErrorMessages(err ? [err] : messages);
+ }, []);
+
+ return (
+
+ );
+};
+
+export const MarkdownEditor = memo(MarkdownEditorComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx
index 481ed7892a8be0..a28abbc8a59e49 100644
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx
@@ -4,20 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState, useCallback } from 'react';
+import React from 'react';
import styled from 'styled-components';
-import {
- EuiMarkdownEditor,
- EuiMarkdownEditorProps,
- EuiFormRow,
- EuiFlexItem,
- EuiFlexGroup,
- getDefaultEuiMarkdownParsingPlugins,
- getDefaultEuiMarkdownProcessingPlugins,
-} from '@elastic/eui';
+import { EuiMarkdownEditorProps, EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports';
-import * as timelineMarkdownPlugin from './plugins/timeline';
+import { MarkdownEditor } from './editor';
type MarkdownEditorFormProps = EuiMarkdownEditorProps & {
id: string;
@@ -34,12 +26,6 @@ const BottomContentWrapper = styled(EuiFlexGroup)`
`}
`;
-export const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
-parsingPlugins.push(timelineMarkdownPlugin.parser);
-
-export const processingPlugins = getDefaultEuiMarkdownProcessingPlugins();
-processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer;
-
export const MarkdownEditorForm: React.FC = ({
id,
field,
@@ -48,10 +34,6 @@ export const MarkdownEditorForm: React.FC = ({
bottomRightContent,
}) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
- const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]);
- const onParse = useCallback((err, { messages }) => {
- setMarkdownErrorMessages(err ? [err] : messages);
- }, []);
return (
= ({
labelAppend={field.labelAppend}
>
<>
-
{bottomRightContent && (
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
index 9f4141dbcae7df..41f5aab691a7a5 100644
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
@@ -5,3 +5,6 @@
*/
export * from './types';
+export * from './renderer';
+export * from './editor';
+export * from './eui_form';
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/markdown_link.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/markdown_link.tsx
new file mode 100644
index 00000000000000..f904b63d4bacea
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/markdown_link.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo } from 'react';
+import { EuiLink, EuiLinkAnchorProps, EuiToolTip } from '@elastic/eui';
+
+type MarkdownLinkProps = { disableLinks?: boolean } & EuiLinkAnchorProps;
+
+/** prevents search engine manipulation by noting the linked document is not trusted or endorsed by us */
+const REL_NOFOLLOW = 'nofollow';
+
+const MarkdownLinkComponent: React.FC = ({
+ disableLinks,
+ href,
+ target,
+ children,
+ ...props
+}) => (
+
+
+ {children}
+
+
+);
+
+export const MarkdownLink = memo(MarkdownLinkComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts
new file mode 100644
index 00000000000000..b3d91d26e50da4
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ getDefaultEuiMarkdownParsingPlugins,
+ getDefaultEuiMarkdownProcessingPlugins,
+} from '@elastic/eui';
+
+import * as timelineMarkdownPlugin from './timeline';
+
+export const uiPlugins = [timelineMarkdownPlugin.plugin];
+export const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
+export const processingPlugins = getDefaultEuiMarkdownProcessingPlugins();
+
+parsingPlugins.push(timelineMarkdownPlugin.parser);
+
+// This line of code is TS-compatible and it will break if [1][1] change in the future.
+processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer;
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx
new file mode 100644
index 00000000000000..e6a38863d7e5f6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { MarkdownRenderer } from './renderer';
+
+describe('Markdown', () => {
+ describe('markdown links', () => {
+ const markdownWithLink = 'A link to an external site [External Site](https://google.com)';
+
+ test('it renders the expected link text', () => {
+ const wrapper = mount({markdownWithLink} );
+
+ expect(wrapper.find('[data-test-subj="markdown-link"]').first().text()).toEqual(
+ 'External Site'
+ );
+ });
+
+ test('it renders the expected href', () => {
+ const wrapper = mount({markdownWithLink} );
+
+ expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
+ 'href',
+ 'https://google.com/'
+ );
+ });
+
+ test('it does NOT render the href if links are disabled', () => {
+ const wrapper = mount(
+ {markdownWithLink}
+ );
+
+ expect(
+ wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()
+ ).not.toHaveProperty('href');
+ });
+
+ test('it opens links in a new tab via target="_blank"', () => {
+ const wrapper = mount({markdownWithLink} );
+
+ expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
+ 'target',
+ '_blank'
+ );
+ });
+
+ test('it sets the link `rel` attribute to `noopener` to prevent the new page from accessing `window.opener`, `nofollow` to note the link is not endorsed by us, and noreferrer to prevent the browser from sending the current address', () => {
+ const wrapper = mount({markdownWithLink} );
+
+ expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
+ 'rel',
+ 'nofollow noopener noreferrer'
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx
new file mode 100644
index 00000000000000..7a7693512afbe9
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo, useMemo } from 'react';
+import { cloneDeep } from 'lodash/fp';
+import { EuiMarkdownFormat, EuiLinkAnchorProps } from '@elastic/eui';
+
+import { parsingPlugins, processingPlugins } from './plugins';
+import { MarkdownLink } from './markdown_link';
+
+interface Props {
+ children: string;
+ disableLinks?: boolean;
+}
+
+const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) => {
+ const MarkdownLinkProcessingComponent: React.FC = useMemo(
+ () => (props) => ,
+ [disableLinks]
+ );
+
+ // Deep clone of the processing plugins to prevent affecting the markdown editor.
+ const processingPluginList = cloneDeep(processingPlugins);
+ // This line of code is TS-compatible and it will break if [1][1] change in the future.
+ processingPluginList[1][1].components.a = MarkdownLinkProcessingComponent;
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const MarkdownRenderer = memo(MarkdownRendererComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx
index 54e6e1cdc11852..772b8b94ad033c 100644
--- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx
@@ -174,6 +174,29 @@ describe('Sourcerer component', () => {
wrapper.find(`[data-test-subj="sourcerer-reset"]`).first().simulate('click');
expect(wrapper.find(`[data-test-subj="config-option"]`).first().exists()).toBeFalsy();
});
+ it('disables saving when no index patterns are selected', () => {
+ store = createStore(
+ {
+ ...state,
+ sourcerer: {
+ ...state.sourcerer,
+ kibanaIndexPatterns: [{ id: '1234', title: 'auditbeat-*' }],
+ },
+ },
+ SUB_PLUGINS_REDUCER,
+ apolloClientObservable,
+ kibanaObservable,
+ storage
+ );
+ const wrapper = mount(
+
+
+
+ );
+ wrapper.find('[data-test-subj="sourcerer-trigger"]').first().simulate('click');
+ wrapper.find('[data-test-subj="comboBoxClearButton"]').first().simulate('click');
+ expect(wrapper.find('[data-test-subj="add-index"]').first().prop('disabled')).toBeTruthy();
+ });
it('returns index pattern options for kibanaIndexPatterns and configIndexPatterns', () => {
store = createStore(
{
diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx
index bc091167983447..52161130d5b3ed 100644
--- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx
@@ -54,6 +54,7 @@ export const Sourcerer = React.memo(({ scope: scopeId }
value: indexSelected,
}))
);
+ const isSavingDisabled = useMemo(() => selectedOptions.length === 0, [selectedOptions]);
const setPopoverIsOpenCb = useCallback(() => setPopoverIsOpen((prevState) => !prevState), []);
@@ -205,6 +206,7 @@ export const Sourcerer = React.memo(({ scope: scopeId }
> => ({
+ id: 'some-id',
+ rawResponse: {
+ body: {
+ hits: {
+ events: [
+ {
+ _index: 'index',
+ _id: '1',
+ _source: {
+ '@timestamp': '2020-10-04T15:16:54.368707900Z',
+ },
+ },
+ {
+ _index: 'index',
+ _id: '2',
+ _source: {
+ '@timestamp': '2020-10-04T15:50:54.368707900Z',
+ },
+ },
+ {
+ _index: 'index',
+ _id: '3',
+ _source: {
+ '@timestamp': '2020-10-04T15:06:54.368707900Z',
+ },
+ },
+ {
+ _index: 'index',
+ _id: '4',
+ _source: {
+ '@timestamp': '2020-10-04T15:15:54.368707900Z',
+ },
+ },
+ ],
+ total: {
+ value: 4,
+ relation: '',
+ },
+ },
+ is_partial: false,
+ is_running: false,
+ took: 300,
+ timed_out: false,
+ },
+ headers: {},
+ warnings: [],
+ meta: {
+ aborted: false,
+ attempts: 0,
+ context: null,
+ name: 'elasticsearch-js',
+ connection: {} as Connection,
+ request: {
+ params: {
+ body: JSON.stringify({
+ filter: {
+ range: {
+ '@timestamp': {
+ gte: '2020-10-07T00:46:12.414Z',
+ lte: '2020-10-07T01:46:12.414Z',
+ format: 'strict_date_optional_time',
+ },
+ },
+ },
+ }),
+ method: 'GET',
+ path: '/_eql/search/',
+ querystring: 'some query string',
+ },
+ options: {},
+ id: '',
+ },
+ },
+ statusCode: 200,
+ },
+});
+
+export const getMockEqlSequenceResponse = (): EqlSearchStrategyResponse<
+ EqlSearchResponse
+> => ({
+ id: 'some-id',
+ rawResponse: {
+ body: {
+ hits: {
+ sequences: [
+ {
+ join_keys: [],
+ events: [
+ {
+ _index: 'index',
+ _id: '1',
+ _source: {
+ '@timestamp': '2020-10-04T15:16:54.368707900Z',
+ },
+ },
+ {
+ _index: 'index',
+ _id: '2',
+ _source: {
+ '@timestamp': '2020-10-04T15:50:54.368707900Z',
+ },
+ },
+ ],
+ },
+ {
+ join_keys: [],
+ events: [
+ {
+ _index: 'index',
+ _id: '3',
+ _source: {
+ '@timestamp': '2020-10-04T15:06:54.368707900Z',
+ },
+ },
+ {
+ _index: 'index',
+ _id: '4',
+ _source: {
+ '@timestamp': '2020-10-04T15:15:54.368707900Z',
+ },
+ },
+ ],
+ },
+ ],
+ total: {
+ value: 4,
+ relation: '',
+ },
+ },
+ is_partial: false,
+ is_running: false,
+ took: 300,
+ timed_out: false,
+ },
+ headers: {},
+ warnings: [],
+ meta: {
+ aborted: false,
+ attempts: 0,
+ context: null,
+ name: 'elasticsearch-js',
+ connection: {} as Connection,
+ request: {
+ params: {
+ body: JSON.stringify({
+ filter: {
+ range: {
+ '@timestamp': {
+ gte: '2020-10-07T00:46:12.414Z',
+ lte: '2020-10-07T01:46:12.414Z',
+ format: 'strict_date_optional_time',
+ },
+ },
+ },
+ }),
+ method: 'GET',
+ path: '/_eql/search/',
+ querystring: 'some query string',
+ },
+ options: {},
+ id: '',
+ },
+ },
+ statusCode: 200,
+ },
+});
diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts
index 07e8caa0bf0b96..6ba2eaa3d3c30c 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts
@@ -3,7 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import dateMath from '@elastic/datemath';
import moment from 'moment';
import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common';
@@ -17,175 +16,44 @@ import {
getEqlAggsData,
createIntervalArray,
getInterval,
- getSequenceAggs,
+ formatInspect,
+ getEventsToBucket,
} from './helpers';
-
-export const getMockResponse = (): EqlSearchStrategyResponse> =>
- ({
- id: 'some-id',
- rawResponse: {
- body: {
- hits: {
- events: [
- {
- _index: 'index',
- _id: '1',
- _source: {
- '@timestamp': '2020-10-04T15:16:54.368707900Z',
- },
- },
- {
- _index: 'index',
- _id: '2',
- _source: {
- '@timestamp': '2020-10-04T15:50:54.368707900Z',
- },
- },
- {
- _index: 'index',
- _id: '3',
- _source: {
- '@timestamp': '2020-10-04T15:06:54.368707900Z',
- },
- },
- {
- _index: 'index',
- _id: '4',
- _source: {
- '@timestamp': '2020-10-04T15:15:54.368707900Z',
- },
- },
- ],
- total: {
- value: 4,
- relation: '',
- },
- },
- },
- meta: {
- request: {
- params: {
- method: 'GET',
- path: '/_eql/search/',
- },
- options: {},
- id: '',
- },
- },
- statusCode: 200,
- },
- } as EqlSearchStrategyResponse>);
-
-const getMockSequenceResponse = (): EqlSearchStrategyResponse> =>
- (({
- id: 'some-id',
- rawResponse: {
- body: {
- hits: {
- sequences: [
- {
- join_keys: [],
- events: [
- {
- _index: 'index',
- _id: '1',
- _source: {
- '@timestamp': '2020-10-04T15:16:54.368707900Z',
- },
- },
- {
- _index: 'index',
- _id: '2',
- _source: {
- '@timestamp': '2020-10-04T15:50:54.368707900Z',
- },
- },
- ],
- },
- {
- join_keys: [],
- events: [
- {
- _index: 'index',
- _id: '3',
- _source: {
- '@timestamp': '2020-10-04T15:06:54.368707900Z',
- },
- },
- {
- _index: 'index',
- _id: '4',
- _source: {
- '@timestamp': '2020-10-04T15:15:54.368707900Z',
- },
- },
- ],
- },
- ],
- total: {
- value: 4,
- relation: '',
- },
- },
- },
- meta: {
- request: {
- params: {
- body: JSON.stringify({
- filter: {
- range: {
- '@timestamp': {
- gte: '2020-10-07T00:46:12.414Z',
- lte: '2020-10-07T01:46:12.414Z',
- format: 'strict_date_optional_time',
- },
- },
- },
- }),
- method: 'GET',
- path: '/_eql/search/',
- },
- options: {},
- id: '',
- },
- },
- statusCode: 200,
- },
- } as unknown) as EqlSearchStrategyResponse>);
+import { getMockEqlResponse, getMockEqlSequenceResponse } from './eql_search_response.mock';
describe('eql/helpers', () => {
describe('calculateBucketForHour', () => {
- test('returns 2 if event occurred within 2 minutes of "now"', () => {
+ test('returns 2 if the difference in times is 2 minutes', () => {
const diff = calculateBucketForHour(
- Number(dateMath.parse('now-1m')?.format('x')),
- Number(dateMath.parse('now')?.format('x'))
+ Date.parse('2020-02-20T05:56:54.037Z'),
+ Date.parse('2020-02-20T05:57:54.037Z')
);
expect(diff).toEqual(2);
});
- test('returns 10 if event occurred within 8-10 minutes of "now"', () => {
+ test('returns 10 if the difference in times is 8-10 minutes', () => {
const diff = calculateBucketForHour(
- Number(dateMath.parse('now-9m')?.format('x')),
- Number(dateMath.parse('now')?.format('x'))
+ Date.parse('2020-02-20T05:48:54.037Z'),
+ Date.parse('2020-02-20T05:57:54.037Z')
);
expect(diff).toEqual(10);
});
- test('returns 16 if event occurred within 10-15 minutes of "now"', () => {
+ test('returns 16 if the difference in times is 10-15 minutes', () => {
const diff = calculateBucketForHour(
- Number(dateMath.parse('now-15m')?.format('x')),
- Number(dateMath.parse('now')?.format('x'))
+ Date.parse('2020-02-20T05:42:54.037Z'),
+ Date.parse('2020-02-20T05:57:54.037Z')
);
expect(diff).toEqual(16);
});
- test('returns 60 if event occurred within 58-60 minutes of "now"', () => {
+ test('returns 60 if the difference in times is 58-60 minutes', () => {
const diff = calculateBucketForHour(
- Number(dateMath.parse('now-59m')?.format('x')),
- Number(dateMath.parse('now')?.format('x'))
+ Date.parse('2020-02-20T04:58:54.037Z'),
+ Date.parse('2020-02-20T05:57:54.037Z')
);
expect(diff).toEqual(60);
@@ -193,8 +61,8 @@ describe('eql/helpers', () => {
test('returns exact time difference if it is a multiple of 2', () => {
const diff = calculateBucketForHour(
- Number(dateMath.parse('now-20m')?.format('x')),
- Number(dateMath.parse('now')?.format('x'))
+ Date.parse('2020-02-20T05:37:54.037Z'),
+ Date.parse('2020-02-20T05:57:54.037Z')
);
expect(diff).toEqual(20);
@@ -202,251 +70,488 @@ describe('eql/helpers', () => {
test('returns 0 if times are equal', () => {
const diff = calculateBucketForHour(
- Number(dateMath.parse('now')?.format('x')),
- Number(dateMath.parse('now')?.format('x'))
+ Date.parse('2020-02-20T05:57:54.037Z'),
+ Date.parse('2020-02-20T05:57:54.037Z')
);
expect(diff).toEqual(0);
});
+
+ test('returns 2 if the difference in times is 2 minutes but arguments are flipped', () => {
+ const diff = calculateBucketForHour(
+ Date.parse('2020-02-20T05:57:54.037Z'),
+ Date.parse('2020-02-20T05:56:54.037Z')
+ );
+
+ expect(diff).toEqual(2);
+ });
});
describe('calculateBucketForDay', () => {
test('returns 0 if two dates are equivalent', () => {
const diff = calculateBucketForDay(
- Number(dateMath.parse('now')?.format('x')),
- Number(dateMath.parse('now')?.format('x'))
+ Date.parse('2020-02-20T05:57:54.037Z'),
+ Date.parse('2020-02-20T05:57:54.037Z')
);
expect(diff).toEqual(0);
});
- test('returns 1 if event occurred within 60 minutes of "now"', () => {
+ test('returns 1 if the difference in times is 60 minutes', () => {
const diff = calculateBucketForDay(
- Number(dateMath.parse('now-40m')?.format('x')),
- Number(dateMath.parse('now')?.format('x'))
+ Date.parse('2020-02-20T05:17:54.037Z'),
+ Date.parse('2020-02-20T05:57:54.037Z')
);
expect(diff).toEqual(1);
});
- test('returns 2 if event occurred 60-120 minutes from "now"', () => {
+ test('returns 2 if the difference in times is 60-120 minutes', () => {
const diff = calculateBucketForDay(
- Number(dateMath.parse('now-120m')?.format('x')),
- Number(dateMath.parse('now')?.format('x'))
+ Date.parse('2020-02-20T03:57:54.037Z'),
+ Date.parse('2020-02-20T05:57:54.037Z')
);
expect(diff).toEqual(2);
});
- test('returns 3 if event occurred 120-180 minutes from "now', () => {
+ test('returns 3 if the difference in times is 120-180 minutes', () => {
const diff = calculateBucketForDay(
- Number(dateMath.parse('now-121m')?.format('x')),
- Number(dateMath.parse('now')?.format('x'))
+ Date.parse('2020-02-20T03:56:54.037Z'),
+ Date.parse('2020-02-20T05:57:54.037Z')
);
expect(diff).toEqual(3);
});
- test('returns 4 if event occurred 180-240 minutes from "now', () => {
+ test('returns 4 if the difference in times is 180-240 minutes', () => {
const diff = calculateBucketForDay(
- Number(dateMath.parse('now-220m')?.format('x')),
- Number(dateMath.parse('now')?.format('x'))
+ Date.parse('2020-02-20T02:15:54.037Z'),
+ Date.parse('2020-02-20T05:57:54.037Z')
);
expect(diff).toEqual(4);
});
- });
- describe('getEqlAggsData', () => {
- test('it returns results bucketed into 2 min intervals when range is "h"', () => {
- const mockResponse = getMockResponse();
-
- const aggs = getEqlAggsData(
- mockResponse,
- 'h',
- '2020-10-04T16:00:00.368707900Z',
- jest.fn() as inputsModel.Refetch
+ test('returns 2 if the difference in times is 60-120 minutes but arguments are flipped', () => {
+ const diff = calculateBucketForDay(
+ Date.parse('2020-02-20T05:57:54.037Z'),
+ Date.parse('2020-02-20T03:59:54.037Z')
);
- const date1 = moment(aggs.data[0].x);
- const date2 = moment(aggs.data[1].x);
- // This'll be in ms
- const diff = date1.diff(date2);
-
- expect(diff).toEqual(120000);
- expect(aggs.data).toHaveLength(31);
- expect(aggs.data).toEqual([
- { g: 'hits', x: 1601827200368, y: 0 },
- { g: 'hits', x: 1601827080368, y: 0 },
- { g: 'hits', x: 1601826960368, y: 0 },
- { g: 'hits', x: 1601826840368, y: 0 },
- { g: 'hits', x: 1601826720368, y: 0 },
- { g: 'hits', x: 1601826600368, y: 1 },
- { g: 'hits', x: 1601826480368, y: 0 },
- { g: 'hits', x: 1601826360368, y: 0 },
- { g: 'hits', x: 1601826240368, y: 0 },
- { g: 'hits', x: 1601826120368, y: 0 },
- { g: 'hits', x: 1601826000368, y: 0 },
- { g: 'hits', x: 1601825880368, y: 0 },
- { g: 'hits', x: 1601825760368, y: 0 },
- { g: 'hits', x: 1601825640368, y: 0 },
- { g: 'hits', x: 1601825520368, y: 0 },
- { g: 'hits', x: 1601825400368, y: 0 },
- { g: 'hits', x: 1601825280368, y: 0 },
- { g: 'hits', x: 1601825160368, y: 0 },
- { g: 'hits', x: 1601825040368, y: 0 },
- { g: 'hits', x: 1601824920368, y: 0 },
- { g: 'hits', x: 1601824800368, y: 0 },
- { g: 'hits', x: 1601824680368, y: 0 },
- { g: 'hits', x: 1601824560368, y: 2 },
- { g: 'hits', x: 1601824440368, y: 0 },
- { g: 'hits', x: 1601824320368, y: 0 },
- { g: 'hits', x: 1601824200368, y: 0 },
- { g: 'hits', x: 1601824080368, y: 0 },
- { g: 'hits', x: 1601823960368, y: 1 },
- { g: 'hits', x: 1601823840368, y: 0 },
- { g: 'hits', x: 1601823720368, y: 0 },
- { g: 'hits', x: 1601823600368, y: 0 },
- ]);
+ expect(diff).toEqual(2);
});
+ });
- test('it returns results bucketed into 1 hour intervals when range is "d"', () => {
- const mockResponse = getMockResponse();
- const response = {
- ...mockResponse,
- rawResponse: {
- ...mockResponse.rawResponse,
- body: {
- is_partial: false,
- is_running: false,
- timed_out: false,
- took: 15,
- hits: {
- events: [
- {
- _index: 'index',
- _id: '1',
- _source: {
- '@timestamp': '2020-10-04T15:16:54.368707900Z',
+ describe('getEqlAggsData', () => {
+ describe('non-sequence', () => {
+ test('it returns results bucketed into 2 min intervals when range is "h"', () => {
+ const mockResponse = getMockEqlResponse();
+
+ const aggs = getEqlAggsData(
+ mockResponse,
+ 'h',
+ '2020-10-04T16:00:00.368707900Z',
+ jest.fn() as inputsModel.Refetch,
+ ['foo-*'],
+ false
+ );
+
+ const date1 = moment(aggs.data[0].x);
+ const date2 = moment(aggs.data[1].x);
+ // This will be in ms
+ const diff = date1.diff(date2);
+
+ expect(diff).toEqual(120000);
+ expect(aggs.data).toHaveLength(31);
+ expect(aggs.data).toEqual([
+ { g: 'hits', x: 1601827200368, y: 0 },
+ { g: 'hits', x: 1601827080368, y: 0 },
+ { g: 'hits', x: 1601826960368, y: 0 },
+ { g: 'hits', x: 1601826840368, y: 0 },
+ { g: 'hits', x: 1601826720368, y: 0 },
+ { g: 'hits', x: 1601826600368, y: 1 },
+ { g: 'hits', x: 1601826480368, y: 0 },
+ { g: 'hits', x: 1601826360368, y: 0 },
+ { g: 'hits', x: 1601826240368, y: 0 },
+ { g: 'hits', x: 1601826120368, y: 0 },
+ { g: 'hits', x: 1601826000368, y: 0 },
+ { g: 'hits', x: 1601825880368, y: 0 },
+ { g: 'hits', x: 1601825760368, y: 0 },
+ { g: 'hits', x: 1601825640368, y: 0 },
+ { g: 'hits', x: 1601825520368, y: 0 },
+ { g: 'hits', x: 1601825400368, y: 0 },
+ { g: 'hits', x: 1601825280368, y: 0 },
+ { g: 'hits', x: 1601825160368, y: 0 },
+ { g: 'hits', x: 1601825040368, y: 0 },
+ { g: 'hits', x: 1601824920368, y: 0 },
+ { g: 'hits', x: 1601824800368, y: 0 },
+ { g: 'hits', x: 1601824680368, y: 0 },
+ { g: 'hits', x: 1601824560368, y: 2 },
+ { g: 'hits', x: 1601824440368, y: 0 },
+ { g: 'hits', x: 1601824320368, y: 0 },
+ { g: 'hits', x: 1601824200368, y: 0 },
+ { g: 'hits', x: 1601824080368, y: 0 },
+ { g: 'hits', x: 1601823960368, y: 1 },
+ { g: 'hits', x: 1601823840368, y: 0 },
+ { g: 'hits', x: 1601823720368, y: 0 },
+ { g: 'hits', x: 1601823600368, y: 0 },
+ ]);
+ });
+
+ test('it returns results bucketed into 1 hour intervals when range is "d"', () => {
+ const mockResponse = getMockEqlResponse();
+ const response: EqlSearchStrategyResponse> = {
+ ...mockResponse,
+ rawResponse: {
+ ...mockResponse.rawResponse,
+ body: {
+ is_partial: false,
+ is_running: false,
+ timed_out: false,
+ took: 15,
+ hits: {
+ events: [
+ {
+ _index: 'index',
+ _id: '1',
+ _source: {
+ '@timestamp': '2020-10-04T15:16:54.368707900Z',
+ },
},
- },
- {
- _index: 'index',
- _id: '2',
- _source: {
- '@timestamp': '2020-10-04T05:50:54.368707900Z',
+ {
+ _index: 'index',
+ _id: '2',
+ _source: {
+ '@timestamp': '2020-10-04T05:50:54.368707900Z',
+ },
},
- },
- {
- _index: 'index',
- _id: '3',
- _source: {
- '@timestamp': '2020-10-04T18:06:54.368707900Z',
+ {
+ _index: 'index',
+ _id: '3',
+ _source: {
+ '@timestamp': '2020-10-04T18:06:54.368707900Z',
+ },
},
- },
- {
- _index: 'index',
- _id: '4',
- _source: {
- '@timestamp': '2020-10-04T23:15:54.368707900Z',
+ {
+ _index: 'index',
+ _id: '4',
+ _source: {
+ '@timestamp': '2020-10-04T23:15:54.368707900Z',
+ },
},
+ ],
+ total: {
+ value: 4,
+ relation: '',
},
- ],
- total: {
- value: 4,
- relation: '',
},
},
},
- },
- };
-
- const aggs = getEqlAggsData(
- response,
- 'd',
- '2020-10-04T23:50:00.368707900Z',
- jest.fn() as inputsModel.Refetch
- );
- const date1 = moment(aggs.data[0].x);
- const date2 = moment(aggs.data[1].x);
- // This'll be in ms
- const diff = date1.diff(date2);
-
- expect(diff).toEqual(3600000);
- expect(aggs.data).toHaveLength(25);
- expect(aggs.data).toEqual([
- { g: 'hits', x: 1601855400368, y: 0 },
- { g: 'hits', x: 1601851800368, y: 1 },
- { g: 'hits', x: 1601848200368, y: 0 },
- { g: 'hits', x: 1601844600368, y: 0 },
- { g: 'hits', x: 1601841000368, y: 0 },
- { g: 'hits', x: 1601837400368, y: 0 },
- { g: 'hits', x: 1601833800368, y: 1 },
- { g: 'hits', x: 1601830200368, y: 0 },
- { g: 'hits', x: 1601826600368, y: 0 },
- { g: 'hits', x: 1601823000368, y: 1 },
- { g: 'hits', x: 1601819400368, y: 0 },
- { g: 'hits', x: 1601815800368, y: 0 },
- { g: 'hits', x: 1601812200368, y: 0 },
- { g: 'hits', x: 1601808600368, y: 0 },
- { g: 'hits', x: 1601805000368, y: 0 },
- { g: 'hits', x: 1601801400368, y: 0 },
- { g: 'hits', x: 1601797800368, y: 0 },
- { g: 'hits', x: 1601794200368, y: 0 },
- { g: 'hits', x: 1601790600368, y: 1 },
- { g: 'hits', x: 1601787000368, y: 0 },
- { g: 'hits', x: 1601783400368, y: 0 },
- { g: 'hits', x: 1601779800368, y: 0 },
- { g: 'hits', x: 1601776200368, y: 0 },
- { g: 'hits', x: 1601772600368, y: 0 },
- { g: 'hits', x: 1601769000368, y: 0 },
- ]);
- });
-
- test('it correctly returns total hits', () => {
- const mockResponse = getMockResponse();
-
- const aggs = getEqlAggsData(
- mockResponse,
- 'h',
- '2020-10-04T16:00:00.368707900Z',
- jest.fn() as inputsModel.Refetch
- );
-
- expect(aggs.totalCount).toEqual(4);
+ };
+
+ const aggs = getEqlAggsData(
+ response,
+ 'd',
+ '2020-10-04T23:50:00.368707900Z',
+ jest.fn() as inputsModel.Refetch,
+ ['foo-*'],
+ false
+ );
+ const date1 = moment(aggs.data[0].x);
+ const date2 = moment(aggs.data[1].x);
+ // This'll be in ms
+ const diff = date1.diff(date2);
+
+ expect(diff).toEqual(3600000);
+ expect(aggs.data).toHaveLength(25);
+ expect(aggs.data).toEqual([
+ { g: 'hits', x: 1601855400368, y: 0 },
+ { g: 'hits', x: 1601851800368, y: 1 },
+ { g: 'hits', x: 1601848200368, y: 0 },
+ { g: 'hits', x: 1601844600368, y: 0 },
+ { g: 'hits', x: 1601841000368, y: 0 },
+ { g: 'hits', x: 1601837400368, y: 0 },
+ { g: 'hits', x: 1601833800368, y: 1 },
+ { g: 'hits', x: 1601830200368, y: 0 },
+ { g: 'hits', x: 1601826600368, y: 0 },
+ { g: 'hits', x: 1601823000368, y: 1 },
+ { g: 'hits', x: 1601819400368, y: 0 },
+ { g: 'hits', x: 1601815800368, y: 0 },
+ { g: 'hits', x: 1601812200368, y: 0 },
+ { g: 'hits', x: 1601808600368, y: 0 },
+ { g: 'hits', x: 1601805000368, y: 0 },
+ { g: 'hits', x: 1601801400368, y: 0 },
+ { g: 'hits', x: 1601797800368, y: 0 },
+ { g: 'hits', x: 1601794200368, y: 0 },
+ { g: 'hits', x: 1601790600368, y: 1 },
+ { g: 'hits', x: 1601787000368, y: 0 },
+ { g: 'hits', x: 1601783400368, y: 0 },
+ { g: 'hits', x: 1601779800368, y: 0 },
+ { g: 'hits', x: 1601776200368, y: 0 },
+ { g: 'hits', x: 1601772600368, y: 0 },
+ { g: 'hits', x: 1601769000368, y: 0 },
+ ]);
+ });
+
+ test('it correctly returns total hits', () => {
+ const mockResponse = getMockEqlResponse();
+
+ const aggs = getEqlAggsData(
+ mockResponse,
+ 'h',
+ '2020-10-04T16:00:00.368707900Z',
+ jest.fn() as inputsModel.Refetch,
+ ['foo-*'],
+ false
+ );
+
+ expect(aggs.totalCount).toEqual(4);
+ });
+
+ test('it returns array with each item having a "total" of 0 if response returns no hits', () => {
+ const mockResponse = getMockEqlResponse();
+ const response: EqlSearchStrategyResponse> = {
+ ...mockResponse,
+ rawResponse: {
+ ...mockResponse.rawResponse,
+ body: {
+ is_partial: false,
+ is_running: false,
+ timed_out: false,
+ took: 15,
+ hits: {
+ total: {
+ value: 0,
+ relation: '',
+ },
+ },
+ },
+ },
+ };
+
+ const aggs = getEqlAggsData(
+ response,
+ 'h',
+ '2020-10-04T16:00:00.368707900Z',
+ jest.fn() as inputsModel.Refetch,
+ ['foo-*'],
+ false
+ );
+
+ expect(aggs.data.every(({ y }) => y === 0)).toBeTruthy();
+ expect(aggs.totalCount).toEqual(0);
+ });
});
- test('it returns array with each item having a "total" of 0 if response returns no hits', () => {
- const mockResponse = getMockResponse();
- const response = {
- ...mockResponse,
- rawResponse: {
- ...mockResponse.rawResponse,
- body: {
- id: 'some-id',
- is_partial: false,
- is_running: false,
- timed_out: false,
- took: 15,
- hits: {
- total: {
- value: 0,
- relation: '',
+ describe('sequence', () => {
+ test('it returns results bucketed into 2 min intervals when range is "h"', () => {
+ const mockResponse = getMockEqlSequenceResponse();
+
+ const aggs = getEqlAggsData(
+ mockResponse,
+ 'h',
+ '2020-10-04T16:00:00.368707900Z',
+ jest.fn() as inputsModel.Refetch,
+ ['foo-*'],
+ true
+ );
+
+ const date1 = moment(aggs.data[0].x);
+ const date2 = moment(aggs.data[1].x);
+ // This will be in ms
+ const diff = date1.diff(date2);
+
+ expect(diff).toEqual(120000);
+ expect(aggs.data).toHaveLength(31);
+ expect(aggs.data).toEqual([
+ { g: 'hits', x: 1601827200368, y: 0 },
+ { g: 'hits', x: 1601827080368, y: 0 },
+ { g: 'hits', x: 1601826960368, y: 0 },
+ { g: 'hits', x: 1601826840368, y: 0 },
+ { g: 'hits', x: 1601826720368, y: 0 },
+ { g: 'hits', x: 1601826600368, y: 1 },
+ { g: 'hits', x: 1601826480368, y: 0 },
+ { g: 'hits', x: 1601826360368, y: 0 },
+ { g: 'hits', x: 1601826240368, y: 0 },
+ { g: 'hits', x: 1601826120368, y: 0 },
+ { g: 'hits', x: 1601826000368, y: 0 },
+ { g: 'hits', x: 1601825880368, y: 0 },
+ { g: 'hits', x: 1601825760368, y: 0 },
+ { g: 'hits', x: 1601825640368, y: 0 },
+ { g: 'hits', x: 1601825520368, y: 0 },
+ { g: 'hits', x: 1601825400368, y: 0 },
+ { g: 'hits', x: 1601825280368, y: 0 },
+ { g: 'hits', x: 1601825160368, y: 0 },
+ { g: 'hits', x: 1601825040368, y: 0 },
+ { g: 'hits', x: 1601824920368, y: 0 },
+ { g: 'hits', x: 1601824800368, y: 0 },
+ { g: 'hits', x: 1601824680368, y: 0 },
+ { g: 'hits', x: 1601824560368, y: 1 },
+ { g: 'hits', x: 1601824440368, y: 0 },
+ { g: 'hits', x: 1601824320368, y: 0 },
+ { g: 'hits', x: 1601824200368, y: 0 },
+ { g: 'hits', x: 1601824080368, y: 0 },
+ { g: 'hits', x: 1601823960368, y: 0 },
+ { g: 'hits', x: 1601823840368, y: 0 },
+ { g: 'hits', x: 1601823720368, y: 0 },
+ { g: 'hits', x: 1601823600368, y: 0 },
+ ]);
+ });
+
+ test('it returns results bucketed into 1 hour intervals when range is "d"', () => {
+ const mockResponse = getMockEqlSequenceResponse();
+ const response: EqlSearchStrategyResponse> = {
+ ...mockResponse,
+ rawResponse: {
+ ...mockResponse.rawResponse,
+ body: {
+ is_partial: false,
+ is_running: false,
+ timed_out: false,
+ took: 15,
+ hits: {
+ sequences: [
+ {
+ join_keys: [],
+ events: [
+ {
+ _index: 'index',
+ _id: '1',
+ _source: {
+ '@timestamp': '2020-10-04T15:16:54.368707900Z',
+ },
+ },
+ {
+ _index: 'index',
+ _id: '2',
+ _source: {
+ '@timestamp': '2020-10-04T05:50:54.368707900Z',
+ },
+ },
+ ],
+ },
+ {
+ join_keys: [],
+ events: [
+ {
+ _index: 'index',
+ _id: '3',
+ _source: {
+ '@timestamp': '2020-10-04T18:06:54.368707900Z',
+ },
+ },
+ {
+ _index: 'index',
+ _id: '4',
+ _source: {
+ '@timestamp': '2020-10-04T23:15:54.368707900Z',
+ },
+ },
+ ],
+ },
+ ],
+ total: {
+ value: 4,
+ relation: '',
+ },
},
},
},
- },
- };
-
- const aggs = getEqlAggsData(
- response,
- 'h',
- '2020-10-04T16:00:00.368707900Z',
- jest.fn() as inputsModel.Refetch
- );
-
- expect(aggs.data.every(({ y }) => y === 0)).toBeTruthy();
- expect(aggs.totalCount).toEqual(0);
+ };
+
+ const aggs = getEqlAggsData(
+ response,
+ 'd',
+ '2020-10-04T23:50:00.368707900Z',
+ jest.fn() as inputsModel.Refetch,
+ ['foo-*'],
+ true
+ );
+ const date1 = moment(aggs.data[0].x);
+ const date2 = moment(aggs.data[1].x);
+ // This'll be in ms
+ const diff = date1.diff(date2);
+
+ expect(diff).toEqual(3600000);
+ expect(aggs.data).toHaveLength(25);
+ expect(aggs.data).toEqual([
+ { g: 'hits', x: 1601855400368, y: 0 },
+ { g: 'hits', x: 1601851800368, y: 1 },
+ { g: 'hits', x: 1601848200368, y: 0 },
+ { g: 'hits', x: 1601844600368, y: 0 },
+ { g: 'hits', x: 1601841000368, y: 0 },
+ { g: 'hits', x: 1601837400368, y: 0 },
+ { g: 'hits', x: 1601833800368, y: 0 },
+ { g: 'hits', x: 1601830200368, y: 0 },
+ { g: 'hits', x: 1601826600368, y: 0 },
+ { g: 'hits', x: 1601823000368, y: 0 },
+ { g: 'hits', x: 1601819400368, y: 0 },
+ { g: 'hits', x: 1601815800368, y: 0 },
+ { g: 'hits', x: 1601812200368, y: 0 },
+ { g: 'hits', x: 1601808600368, y: 0 },
+ { g: 'hits', x: 1601805000368, y: 0 },
+ { g: 'hits', x: 1601801400368, y: 0 },
+ { g: 'hits', x: 1601797800368, y: 0 },
+ { g: 'hits', x: 1601794200368, y: 0 },
+ { g: 'hits', x: 1601790600368, y: 1 },
+ { g: 'hits', x: 1601787000368, y: 0 },
+ { g: 'hits', x: 1601783400368, y: 0 },
+ { g: 'hits', x: 1601779800368, y: 0 },
+ { g: 'hits', x: 1601776200368, y: 0 },
+ { g: 'hits', x: 1601772600368, y: 0 },
+ { g: 'hits', x: 1601769000368, y: 0 },
+ ]);
+ });
+
+ test('it correctly returns total hits', () => {
+ const mockResponse = getMockEqlSequenceResponse();
+
+ const aggs = getEqlAggsData(
+ mockResponse,
+ 'h',
+ '2020-10-04T16:00:00.368707900Z',
+ jest.fn() as inputsModel.Refetch,
+ ['foo-*'],
+ true
+ );
+
+ expect(aggs.totalCount).toEqual(4);
+ });
+
+ test('it returns array with each item having a "total" of 0 if response returns no hits', () => {
+ const mockResponse = getMockEqlSequenceResponse();
+ const response: EqlSearchStrategyResponse> = {
+ ...mockResponse,
+ rawResponse: {
+ ...mockResponse.rawResponse,
+ body: {
+ is_partial: false,
+ is_running: false,
+ timed_out: false,
+ took: 15,
+ hits: {
+ total: {
+ value: 0,
+ relation: '',
+ },
+ },
+ },
+ },
+ };
+
+ const aggs = getEqlAggsData(
+ response,
+ 'h',
+ '2020-10-04T16:00:00.368707900Z',
+ jest.fn() as inputsModel.Refetch,
+ ['foo-*'],
+ true
+ );
+
+ expect(aggs.data.every(({ y }) => y === 0)).toBeTruthy();
+ expect(aggs.totalCount).toEqual(0);
+ });
});
});
@@ -456,41 +561,9 @@ describe('eql/helpers', () => {
expect(arrayOfNumbers).toEqual([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]);
});
- test('returns array of 30 numbers from 0 to 60 by 2', () => {
- const arrayOfNumbers = createIntervalArray(0, 30, 2);
- expect(arrayOfNumbers).toEqual([
- 0,
- 2,
- 4,
- 6,
- 8,
- 10,
- 12,
- 14,
- 16,
- 18,
- 20,
- 22,
- 24,
- 26,
- 28,
- 30,
- 32,
- 34,
- 36,
- 38,
- 40,
- 42,
- 44,
- 46,
- 48,
- 50,
- 52,
- 54,
- 56,
- 58,
- 60,
- ]);
+ test('returns array of 5 numbers from 0 to 10 by 2', () => {
+ const arrayOfNumbers = createIntervalArray(0, 5, 2);
+ expect(arrayOfNumbers).toEqual([0, 2, 4, 6, 8, 10]);
});
test('returns array of numbers from start param to end param if multiplier is 1', () => {
@@ -500,103 +573,46 @@ describe('eql/helpers', () => {
});
describe('getInterval', () => {
- test('returns object with 2 minute interval keys if range is "h"', () => {
- const intervals = getInterval('h', Date.parse('2020-10-04T15:00:00.368707900Z'));
- const keys = Object.keys(intervals);
- const date1 = moment(Number(intervals['0'].timestamp));
- const date2 = moment(Number(intervals['2'].timestamp));
-
- // This'll be in ms
- const diff = date1.diff(date2);
-
- expect(diff).toEqual(120000);
- expect(keys).toEqual([
- '0',
- '2',
- '4',
- '6',
- '8',
- '10',
- '12',
- '14',
- '16',
- '18',
- '20',
- '22',
- '24',
- '26',
- '28',
- '30',
- '32',
- '34',
- '36',
- '38',
- '40',
- '42',
- '44',
- '46',
- '48',
- '50',
- '52',
- '54',
- '56',
- '58',
- '60',
- ]);
- });
-
test('returns object with 2 minute interval timestamps if range is "h"', () => {
const intervals = getInterval('h', 1601856270140);
- const date1 = moment(Number(intervals['0'].timestamp));
- const date2 = moment(Number(intervals['2'].timestamp));
- // This'll be in ms
- const diff = date1.diff(date2);
+ const allAre2MinApart = Object.keys(intervals).every((int) => {
+ const interval1 = intervals[int];
+ const interval2 = intervals[`${Number(int) + 2}`];
+ if (interval1 != null && interval2 != null) {
+ const date1 = moment(Number(interval1.timestamp));
+ const date2 = moment(Number(interval2.timestamp));
+ // This'll be in ms
+ const diff = date1.diff(date2);
- expect(diff).toEqual(120000);
- });
+ return diff === 120000;
+ }
- test('returns object with 1 hour interval keys if range is "d"', () => {
- const intervals = getInterval('d', 1601856270140);
- const keys = Object.keys(intervals);
- expect(keys).toEqual([
- '0',
- '1',
- '2',
- '3',
- '4',
- '5',
- '6',
- '7',
- '8',
- '9',
- '10',
- '11',
- '12',
- '13',
- '14',
- '15',
- '16',
- '17',
- '18',
- '19',
- '20',
- '21',
- '22',
- '23',
- '24',
- ]);
+ return true;
+ });
+
+ expect(allAre2MinApart).toBeTruthy();
});
test('returns object with 1 hour interval timestamps if range is "d"', () => {
const intervals = getInterval('d', 1601856270140);
- const date1 = moment(Number(intervals['0'].timestamp));
- const date2 = moment(Number(intervals['1'].timestamp));
- // This'll be in ms
- const diff = date1.diff(date2);
+ const allAre1HourApart = Object.keys(intervals).every((int) => {
+ const interval1 = intervals[int];
+ const interval2 = intervals[`${Number(int) + 1}`];
+ if (interval1 != null && interval2 != null) {
+ const date1 = moment(Number(interval1.timestamp));
+ const date2 = moment(Number(interval2.timestamp));
+ // This'll be in ms
+ const diff = date1.diff(date2);
+
+ return diff === 3600000;
+ }
- expect(diff).toEqual(3600000);
+ return true;
+ });
+
+ expect(allAre1HourApart).toBeTruthy();
});
test('returns error if range is anything other than "h" or "d"', () => {
@@ -604,17 +620,100 @@ describe('eql/helpers', () => {
});
});
- describe('getSequenceAggs', () => {
- test('it aggregates events by sequences', () => {
- const mockResponse = getMockSequenceResponse();
- const sequenceAggs = getSequenceAggs(mockResponse, jest.fn() as inputsModel.Refetch);
+ describe('formatInspect', () => {
+ test('it should return "dsl" with response params and index info', () => {
+ const { dsl } = formatInspect(getMockEqlResponse(), ['foo-*']);
+
+ expect(JSON.parse(dsl[0])).toEqual({
+ body: {
+ filter: {
+ range: {
+ '@timestamp': {
+ format: 'strict_date_optional_time',
+ gte: '2020-10-07T00:46:12.414Z',
+ lte: '2020-10-07T01:46:12.414Z',
+ },
+ },
+ },
+ },
+ index: ['foo-*'],
+ method: 'GET',
+ path: '/_eql/search/',
+ querystring: 'some query string',
+ });
+ });
+
+ test('it should return "response"', () => {
+ const mockResponse = getMockEqlResponse();
+ const { response } = formatInspect(mockResponse, ['foo-*']);
+
+ expect(JSON.parse(response[0])).toEqual(mockResponse.rawResponse.body);
+ });
+ });
- expect(sequenceAggs.data).toEqual([
- { g: 'Seq. 1', x: '2020-10-04T15:16:54.368707900Z', y: 1 },
- { g: 'Seq. 1', x: '2020-10-04T15:50:54.368707900Z', y: 1 },
- { g: 'Seq. 2', x: '2020-10-04T15:06:54.368707900Z', y: 1 },
- { g: 'Seq. 2', x: '2020-10-04T15:15:54.368707900Z', y: 1 },
+ describe('getEventsToBucket', () => {
+ test('returns events for non-sequence queries', () => {
+ const events = getEventsToBucket(false, getMockEqlResponse());
+
+ expect(events).toEqual([
+ { _id: '1', _index: 'index', _source: { '@timestamp': '2020-10-04T15:16:54.368707900Z' } },
+ { _id: '2', _index: 'index', _source: { '@timestamp': '2020-10-04T15:50:54.368707900Z' } },
+ { _id: '3', _index: 'index', _source: { '@timestamp': '2020-10-04T15:06:54.368707900Z' } },
+ { _id: '4', _index: 'index', _source: { '@timestamp': '2020-10-04T15:15:54.368707900Z' } },
]);
});
+
+ test('returns empty array if no hits', () => {
+ const resp = getMockEqlResponse();
+ const mockResponse: EqlSearchStrategyResponse> = {
+ ...resp,
+ rawResponse: {
+ ...resp.rawResponse,
+ body: {
+ ...resp.rawResponse.body,
+ hits: {
+ total: {
+ value: 0,
+ relation: '',
+ },
+ },
+ },
+ },
+ };
+ const events = getEventsToBucket(false, mockResponse);
+
+ expect(events).toEqual([]);
+ });
+
+ test('returns events for sequence queries', () => {
+ const events = getEventsToBucket(true, getMockEqlSequenceResponse());
+
+ expect(events).toEqual([
+ { _id: '2', _index: 'index', _source: { '@timestamp': '2020-10-04T15:50:54.368707900Z' } },
+ { _id: '4', _index: 'index', _source: { '@timestamp': '2020-10-04T15:15:54.368707900Z' } },
+ ]);
+ });
+
+ test('returns empty array if no sequences', () => {
+ const resp = getMockEqlSequenceResponse();
+ const mockResponse: EqlSearchStrategyResponse> = {
+ ...resp,
+ rawResponse: {
+ ...resp.rawResponse,
+ body: {
+ ...resp.rawResponse.body,
+ hits: {
+ total: {
+ value: 0,
+ relation: '',
+ },
+ },
+ },
+ },
+ };
+ const events = getEventsToBucket(true, mockResponse);
+
+ expect(events).toEqual([]);
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts
index 4b5986d966df3e..d1a29987c8ced4 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts
@@ -10,55 +10,105 @@ import { inputsModel } from '../../../common/store';
import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common';
import { InspectResponse } from '../../../types';
import { EqlPreviewResponse, Source } from './types';
-import { EqlSearchResponse } from '../../../../common/detection_engine/types';
+import { BaseHit, EqlSearchResponse } from '../../../../common/detection_engine/types';
type EqlAggBuckets = Record;
export const EQL_QUERY_EVENT_SIZE = 100;
-// Calculates which 2 min bucket segment, event should be
-// sorted into
+/**
+ * Calculates which 2 min bucket segment, event should be sorted into
+ * @param eventTimestamp The event to be bucketed timestamp
+ * @param relativeNow The timestamp we are using to calculate how far from 'now' event occurred
+ */
export const calculateBucketForHour = (eventTimestamp: number, relativeNow: number): number => {
- const diff: number = relativeNow - eventTimestamp;
- const minutes: number = Math.floor(diff / 60000);
+ const diff = Math.abs(relativeNow - eventTimestamp);
+ const minutes = Math.floor(diff / 60000);
return Math.ceil(minutes / 2) * 2;
};
-// Calculates which 1 hour bucket segment, event should be
-// sorted into
+/**
+ * Calculates which 1 hour bucket segment, event should be sorted into
+ * @param eventTimestamp The event to be bucketed timestamp
+ * @param relativeNow The timestamp we are using to calculate how far from 'now' event occurred
+ */
export const calculateBucketForDay = (eventTimestamp: number, relativeNow: number): number => {
- const diff: number = relativeNow - eventTimestamp;
- const minutes: number = Math.floor(diff / 60000);
+ const diff = Math.abs(relativeNow - eventTimestamp);
+ const minutes = Math.floor(diff / 60000);
return Math.ceil(minutes / 60);
};
+/**
+ * Formats the response for the UI inspect modal
+ * @param response The query search response
+ * @param indices The indices the query searched
+ * TODO: Update eql search strategy to return index in it's meta
+ * params info, currently not being returned, but expected for
+ * inspect modal display
+ */
export const formatInspect = (
- response: EqlSearchStrategyResponse>
+ response: EqlSearchStrategyResponse>,
+ indices: string[]
): InspectResponse => {
const body = response.rawResponse.meta.request.params.body;
- const bodyParse = typeof body === 'string' ? JSON.parse(body) : body;
+ const bodyParse: Record | undefined =
+ typeof body === 'string' ? JSON.parse(body) : body;
return {
dsl: [
- JSON.stringify({ ...response.rawResponse.meta.request.params, body: bodyParse }, null, 2),
+ JSON.stringify(
+ { ...response.rawResponse.meta.request.params, index: indices, body: bodyParse },
+ null,
+ 2
+ ),
],
response: [JSON.stringify(response.rawResponse.body, null, 2)],
};
};
-// NOTE: Eql does not support aggregations, this is an in-memory
-// hand-spun aggregation for the events to give the user a visual
-// representation of their query results
+/**
+ * Gets the events out of the response based on type of query
+ * @param isSequence Is the eql query a sequence query
+ * @param response The query search response
+ */
+export const getEventsToBucket = (
+ isSequence: boolean,
+ response: EqlSearchStrategyResponse>
+): Array> => {
+ const hits = response.rawResponse.body.hits ?? [];
+ if (isSequence) {
+ return (
+ hits.sequences?.map((seq) => {
+ return seq.events[seq.events.length - 1];
+ }) ?? []
+ );
+ } else {
+ return hits.events ?? [];
+ }
+};
+
+/**
+ * Eql does not support aggregations, this is an in-memory
+ * hand-spun aggregation for the events to give the user a visual
+ * representation of their query results
+ * @param response The query search response
+ * @param range User chosen timeframe (last hour, day)
+ * @param to Based on range chosen
+ * @param refetch Callback used in inspect button, ref just passed through
+ * @param indices Indices searched by query
+ * @param isSequence Is the eql query a sequence query
+ */
export const getEqlAggsData = (
response: EqlSearchStrategyResponse>,
range: Unit,
to: string,
- refetch: inputsModel.Refetch
+ refetch: inputsModel.Refetch,
+ indices: string[],
+ isSequence: boolean
): EqlPreviewResponse => {
- const { dsl, response: inspectResponse } = formatInspect(response);
- // The upper bound of the timestamps
+ const { dsl, response: inspectResponse } = formatInspect(response, indices);
const relativeNow = Date.parse(to);
const accumulator = getInterval(range, relativeNow);
- const events = response.rawResponse.body.hits.events ?? [];
+ const events = getEventsToBucket(isSequence, response);
const totalCount = response.rawResponse.body.hits.total.value;
const buckets = events.reduce((acc, hit) => {
@@ -94,12 +144,23 @@ export const getEqlAggsData = (
};
};
-export const createIntervalArray = (start: number, end: number, multiplier: number) => {
+/**
+ * Helper method to create an array to be used for calculating bucket intervals
+ * @param start
+ * @param end
+ * @param multiplier
+ */
+export const createIntervalArray = (start: number, end: number, multiplier: number): number[] => {
return Array(end - start + 1)
.fill(0)
.map((_, idx) => start + idx * multiplier);
};
+/**
+ * Helper method to create an array to be used for calculating bucket intervals
+ * @param range User chosen timeframe (last hour, day)
+ * @param relativeNow Based on range chosen
+ */
export const getInterval = (range: Unit, relativeNow: number): EqlAggBuckets => {
switch (range) {
case 'h':
@@ -117,38 +178,6 @@ export const getInterval = (range: Unit, relativeNow: number): EqlAggBuckets =>
};
}, {});
default:
- throw new Error('Invalid time range selected');
+ throw new RangeError('Invalid time range selected. Must be "Last hour" or "Last day".');
}
};
-
-export const getSequenceAggs = (
- response: EqlSearchStrategyResponse>,
- refetch: inputsModel.Refetch
-): EqlPreviewResponse => {
- const { dsl, response: inspectResponse } = formatInspect(response);
- const sequences = response.rawResponse.body.hits.sequences ?? [];
- const totalCount = response.rawResponse.body.hits.total.value;
-
- const data = sequences.map((sequence, i) => {
- return sequence.events.map((seqEvent) => {
- if (seqEvent._source['@timestamp'] == null) {
- return {};
- }
- return {
- x: seqEvent._source['@timestamp'],
- y: 1,
- g: `Seq. ${i + 1}`,
- };
- });
- });
-
- return {
- data: data.flat(),
- totalCount,
- inspect: {
- dsl,
- response: inspectResponse,
- },
- refetch,
- };
-};
diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts
index ae7a263cc7012c..663791a00940f3 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts
@@ -14,7 +14,7 @@ import { Source } from './types';
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
import { useKibana } from '../../../common/lib/kibana';
import { useEqlPreview } from '.';
-import { getMockResponse } from './helpers.test';
+import { getMockEqlResponse } from './eql_search_response.mock';
jest.mock('../../../common/lib/kibana');
@@ -32,7 +32,9 @@ describe('useEqlPreview', () => {
useKibana().services.notifications.toasts.addWarning = jest.fn();
- (useKibana().services.data.search.search as jest.Mock).mockReturnValue(of(getMockResponse()));
+ (useKibana().services.data.search.search as jest.Mock).mockReturnValue(
+ of(getMockEqlResponse())
+ );
});
it('should initiate hook', async () => {
@@ -96,7 +98,7 @@ describe('useEqlPreview', () => {
it('should not resolve values after search is invoked if component unmounted', async () => {
await act(async () => {
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
- of(getMockResponse()).pipe(delay(5000))
+ of(getMockEqlResponse()).pipe(delay(5000))
);
const { result, waitForNextUpdate, unmount } = renderHook(() => useEqlPreview());
@@ -117,9 +119,11 @@ describe('useEqlPreview', () => {
it('should not resolve new values on search if response is error response', async () => {
await act(async () => {
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
- of({ isRunning: false, isPartial: true } as EqlSearchStrategyResponse<
- EqlSearchResponse
- >)
+ of>>({
+ ...getMockEqlResponse(),
+ isRunning: false,
+ isPartial: true,
+ })
);
const { result, waitForNextUpdate } = renderHook(() => useEqlPreview());
@@ -136,6 +140,30 @@ describe('useEqlPreview', () => {
});
});
+ // TODO: Determine why eql search strategy returns null for meta.params.body
+ // in complete responses, but not in partial responses
+ it('should update inspect information on partial response', async () => {
+ const mockResponse = getMockEqlResponse();
+ await act(async () => {
+ (useKibana().services.data.search.search as jest.Mock).mockReturnValue(
+ of>>({
+ isRunning: true,
+ isPartial: true,
+ rawResponse: mockResponse.rawResponse,
+ })
+ );
+
+ const { result, waitForNextUpdate } = renderHook(() => useEqlPreview());
+
+ await waitForNextUpdate();
+
+ result.current[1](params);
+
+ expect(result.current[2].inspect.dsl.length).toEqual(1);
+ expect(result.current[2].inspect.response.length).toEqual(1);
+ });
+ });
+
it('should add danger toast if search throws', async () => {
await act(async () => {
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts
index 1bfaecdf089bec..93236381753bf0 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts
@@ -14,12 +14,13 @@ import {
AbortError,
isCompleteResponse,
isErrorResponse,
+ isPartialResponse,
} from '../../../../../../../src/plugins/data/common';
import {
EqlSearchStrategyRequest,
EqlSearchStrategyResponse,
} from '../../../../../data_enhanced/common';
-import { getEqlAggsData, getSequenceAggs } from './helpers';
+import { formatInspect, getEqlAggsData } from './helpers';
import { EqlPreviewResponse, EqlPreviewRequest, Source } from './types';
import { hasEqlSequenceQuery } from '../../../../common/detection_engine/utils';
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
@@ -106,13 +107,37 @@ export const useEqlPreview = (): [
if (isCompleteResponse(res)) {
if (!didCancel.current) {
setLoading(false);
- if (hasEqlSequenceQuery(query)) {
- setResponse(getSequenceAggs(res, refetch.current));
- } else {
- setResponse(getEqlAggsData(res, interval, to, refetch.current));
- }
+
+ setResponse((prev) => {
+ const { inspect, ...rest } = getEqlAggsData(
+ res,
+ interval,
+ to,
+ refetch.current,
+ index,
+ hasEqlSequenceQuery(query)
+ );
+ const inspectDsl = prev.inspect.dsl[0] ? prev.inspect.dsl : inspect.dsl;
+ const inspectResp = prev.inspect.response[0]
+ ? prev.inspect.response
+ : inspect.response;
+
+ return {
+ ...prev,
+ ...rest,
+ inspect: {
+ dsl: inspectDsl,
+ response: inspectResp,
+ },
+ };
+ });
}
+
unsubscribeStream.current.next();
+ } else if (isPartialResponse(res)) {
+ // TODO: Eql search strategy partial responses return a value under meta.params.body
+ // but the final/complete response does not, that's why the inspect values are set here
+ setResponse((prev) => ({ ...prev, inspect: formatInspect(res, index) }));
} else if (isErrorResponse(res)) {
setLoading(false);
notifications.toasts.addWarning(i18n.EQL_PREVIEW_FETCH_FAILURE);
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
index e79ef0d1282250..863097a5cd2ee7 100644
--- a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
@@ -25,7 +25,15 @@ export const track: TrackFn = (type, event, count) => {
}
};
-export const initTelemetry = (usageCollection: SetupPlugins['usageCollection'], appId: string) => {
+export const initTelemetry = (
+ {
+ usageCollection,
+ telemetryManagementSection,
+ }: Pick,
+ appId: string
+) => {
+ telemetryManagementSection?.toggleSecuritySolutionExample(true);
+
_track = usageCollection?.reportUiStats?.bind(null, appId) ?? noop;
};
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
index d66d37a020040b..a50167433ccad2 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
@@ -54,6 +54,7 @@ interface OwnProps {
hasIndexWrite: boolean;
from: string;
loading: boolean;
+ onRuleChange?: () => void;
showBuildingBlockAlerts: boolean;
onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void;
to: string;
@@ -75,6 +76,7 @@ export const AlertsTableComponent: React.FC = ({
isSelectAllChecked,
loading,
loadingEventIds,
+ onRuleChange,
selectedEventIds,
setEventsDeleted,
setEventsLoading,
@@ -330,6 +332,7 @@ export const AlertsTableComponent: React.FC = ({
end={to}
headerFilterGroup={headerFilterGroup}
id={timelineId}
+ onRuleChange={onRuleChange}
scopeId={SourcererScopeName.detections}
start={from}
utilityBar={utilityBarCallback}
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
index 1a0b35620c9c98..0315d513ee2600 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
@@ -29,7 +29,7 @@ import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from '../alerts_filter
import { updateAlertStatusAction } from '../actions';
import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types';
import { Ecs } from '../../../../../common/ecs';
-import { AddExceptionModal as AddExceptionModalComponent } from '../../../../common/components/exceptions/add_exception_modal';
+import { AddExceptionModal } from '../../../../common/components/exceptions/add_exception_modal';
import * as i18nCommon from '../../../../common/translations';
import * as i18n from '../translations';
import {
@@ -45,6 +45,7 @@ interface AlertContextMenuProps {
disabled: boolean;
ecsRowData: Ecs;
refetch: inputsModel.Refetch;
+ onRuleChange?: () => void;
timelineId: string;
}
@@ -52,6 +53,7 @@ const AlertContextMenuComponent: React.FC = ({
disabled,
ecsRowData,
refetch,
+ onRuleChange,
timelineId,
}) => {
const dispatch = useDispatch();
@@ -376,7 +378,7 @@ const AlertContextMenuComponent: React.FC = ({
{exceptionModalType != null && ruleId != null && ecsRowData != null && (
- = ({
onCancel={onAddExceptionCancel}
onConfirm={onAddExceptionConfirm}
alertStatus={alertStatus}
+ onRuleChange={onRuleChange}
/>
)}
>
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx
index 01d95fa80ba596..3dc3213d653146 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx
@@ -52,7 +52,7 @@ describe('PreviewCustomQueryHistogram', () => {
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
expect(
wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).prop('subtitle')
- ).toEqual(i18n.PREVIEW_SUBTITLE_LOADING);
+ ).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING);
});
test('it configures data and subtitle', () => {
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx
index 787e8dab393cac..77b6fbb938e208 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx
@@ -54,7 +54,7 @@ export const PreviewCustomQueryHistogram = ({
const subtitle = useMemo(
(): string =>
- isLoading ? i18n.PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
+ isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
[isLoading, totalCount]
);
@@ -67,7 +67,7 @@ export const PreviewCustomQueryHistogram = ({
barConfig={barConfig}
title={i18n.QUERY_GRAPH_HITS_TITLE}
subtitle={subtitle}
- disclaimer={i18n.PREVIEW_QUERY_DISCLAIMER}
+ disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER}
isLoading={isLoading}
data-test-subj="queryPreviewCustomHistogram"
/>
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx
index 16e71485de9a6c..3e7807f423be92 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx
@@ -39,7 +39,6 @@ describe('PreviewEqlQueryHistogram', () => {
{
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
expect(
wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).prop('subtitle')
- ).toEqual(i18n.PREVIEW_SUBTITLE_LOADING);
+ ).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING);
});
test('it configures data and subtitle', () => {
@@ -63,7 +62,6 @@ describe('PreviewEqlQueryHistogram', () => {
{
{
refetch: mockRefetch,
});
});
+
+ test('it displays histogram', () => {
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy();
+ expect(
+ wrapper.find('[data-test-subj="sharedPreviewQueryNoHistogramAvailable"]').exists()
+ ).toBeFalsy();
+ expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy();
+ });
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx
index 8f2774a1342b6a..ed1fd5b7367d4f 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx
@@ -15,7 +15,6 @@ import {
} from '../../../../common/components/charts/common';
import { InspectQuery } from '../../../../common/store/inputs/model';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
-import { hasEqlSequenceQuery } from '../../../../../common/detection_engine/utils';
import { inputsModel } from '../../../../common/store';
import { PreviewHistogram } from './histogram';
@@ -26,7 +25,6 @@ interface PreviewEqlQueryHistogramProps {
from: string;
totalCount: number;
isLoading: boolean;
- query: string;
data: ChartData[];
inspect: InspectQuery;
refetch: inputsModel.Refetch;
@@ -36,7 +34,6 @@ export const PreviewEqlQueryHistogram = ({
from,
to,
totalCount,
- query,
data,
inspect,
refetch,
@@ -50,14 +47,11 @@ export const PreviewEqlQueryHistogram = ({
}
}, [setQuery, inspect, isInitializing, refetch]);
- const barConfig = useMemo(
- (): ChartSeriesConfigs => getHistogramConfig(to, from, hasEqlSequenceQuery(query)),
- [from, to, query]
- );
+ const barConfig = useMemo((): ChartSeriesConfigs => getHistogramConfig(to, from), [from, to]);
const subtitle = useMemo(
(): string =>
- isLoading ? i18n.PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
+ isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
[isLoading, totalCount]
);
@@ -70,7 +64,7 @@ export const PreviewEqlQueryHistogram = ({
barConfig={barConfig}
title={i18n.QUERY_GRAPH_HITS_TITLE}
subtitle={subtitle}
- disclaimer={i18n.PREVIEW_QUERY_DISCLAIMER_EQL}
+ disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER_EQL}
isLoading={isLoading}
data-test-subj="queryPreviewEqlHistogram"
/>
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts
index ed8994a4c44fd2..89d017f4721f6c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts
@@ -16,13 +16,13 @@ import { FieldValueQueryBar } from '../query_bar';
import { ESQuery } from '../../../../../common/typed_json';
import { Filter } from '../../../../../../../../src/plugins/data/common/es_query';
-export const HITS_THRESHOLD: Record = {
- h: 1,
- d: 24,
- M: 730,
-};
-
-export const isNoisy = (hits: number, timeframe: Unit) => {
+/**
+ * Determines whether or not to display noise warning.
+ * Is considered noisy if alerts/hour rate > 1
+ * @param hits Total query search hits
+ * @param timeframe Range selected by user (last hour, day...)
+ */
+export const isNoisy = (hits: number, timeframe: Unit): boolean => {
if (timeframe === 'h') {
return hits > 1;
} else if (timeframe === 'd') {
@@ -34,6 +34,12 @@ export const isNoisy = (hits: number, timeframe: Unit) => {
return false;
};
+/**
+ * Determines what timerange options to show.
+ * Eql sequence queries tend to be slower, so decided
+ * not to include the last month option.
+ * @param ruleType
+ */
export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => {
if (ruleType === 'eql') {
return [
@@ -49,6 +55,13 @@ export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => {
}
};
+/**
+ * Quick little helper to extract the query info from the
+ * queryBar object.
+ * @param queryBar Object containing all query info
+ * @param index Indices searched
+ * @param ruleType
+ */
export const getInfoFromQueryBar = (
queryBar: FieldValueQueryBar,
index: string[],
@@ -88,10 +101,15 @@ export const getInfoFromQueryBar = (
}
};
+/**
+ * Config passed into elastic-charts settings.
+ * @param to
+ * @param from
+ */
export const getHistogramConfig = (
to: string,
from: string,
- showLegend: boolean = false
+ showLegend = false
): ChartSeriesConfigs => {
return {
series: {
@@ -131,7 +149,11 @@ export const getHistogramConfig = (
};
};
-export const getThresholdHistogramConfig = (height: number | undefined): ChartSeriesConfigs => {
+/**
+ * Threshold histogram is displayed a bit differently,
+ * x-axis is not time based, but ordinal.
+ */
+export const getThresholdHistogramConfig = (): ChartSeriesConfigs => {
return {
series: {
xScaleType: ScaleType.Ordinal,
@@ -165,6 +187,6 @@ export const getThresholdHistogramConfig = (height: number | undefined): ChartSe
},
},
},
- customHeight: height ?? 200,
+ customHeight: 200,
};
};
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx
index 87436ad1e6d2bd..26891dae1752a6 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx
@@ -13,9 +13,13 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { TestProviders } from '../../../../common/mock';
import { useKibana } from '../../../../common/lib/kibana';
import { PreviewQuery } from './';
-import { getMockResponse } from '../../../../common/hooks/eql/helpers.test';
+import { getMockEqlResponse } from '../../../../common/hooks/eql/eql_search_response.mock';
+import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram';
+import { useEqlPreview } from '../../../../common/hooks/eql/';
jest.mock('../../../../common/lib/kibana');
+jest.mock('../../../../common/containers/matrix_histogram');
+jest.mock('../../../../common/hooks/eql/');
describe('PreviewQuery', () => {
beforeEach(() => {
@@ -23,7 +27,33 @@ describe('PreviewQuery', () => {
useKibana().services.notifications.toasts.addWarning = jest.fn();
- (useKibana().services.data.search.search as jest.Mock).mockReturnValue(of(getMockResponse()));
+ (useMatrixHistogram as jest.Mock).mockReturnValue([
+ false,
+ {
+ inspect: { dsl: [], response: [] },
+ totalCount: 1,
+ refetch: jest.fn(),
+ data: [],
+ buckets: [],
+ },
+ (useKibana().services.data.search.search as jest.Mock).mockReturnValue(
+ of(getMockEqlResponse())
+ ),
+ ]);
+
+ (useEqlPreview as jest.Mock).mockReturnValue([
+ false,
+ (useKibana().services.data.search.search as jest.Mock).mockReturnValue(
+ of(getMockEqlResponse())
+ ),
+ {
+ inspect: { dsl: [], response: [] },
+ totalCount: 1,
+ refetch: jest.fn(),
+ data: [],
+ buckets: [],
+ },
+ ]);
});
afterEach(() => {
@@ -121,6 +151,42 @@ describe('PreviewQuery', () => {
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
});
+ test('it renders noise warning when rule type is query, timeframe is last hour and hit average is greater than 1/hour', async () => {
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+
+
+ );
+
+ (useMatrixHistogram as jest.Mock).mockReturnValue([
+ false,
+ {
+ inspect: { dsl: [], response: [] },
+ totalCount: 2,
+ refetch: jest.fn(),
+ data: [],
+ buckets: [],
+ },
+ (useKibana().services.data.search.search as jest.Mock).mockReturnValue(
+ of(getMockEqlResponse())
+ ),
+ ]);
+
+ wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
+
+ expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
+ });
+
test('it renders query histogram when rule type is saved_query and preview button clicked', () => {
const wrapper = mount(
({ eui: euiLightVars, darkMode: false })}>
@@ -175,6 +241,42 @@ describe('PreviewQuery', () => {
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeTruthy();
});
+ test('it renders noise warning when rule type is eql, timeframe is last hour and hit average is greater than 1/hour', async () => {
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+
+
+ );
+
+ (useEqlPreview as jest.Mock).mockReturnValue([
+ false,
+ (useKibana().services.data.search.search as jest.Mock).mockReturnValue(
+ of(getMockEqlResponse())
+ ),
+ {
+ inspect: { dsl: [], response: [] },
+ totalCount: 2,
+ refetch: jest.fn(),
+ data: [],
+ buckets: [],
+ },
+ ]);
+
+ wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
+
+ expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
+ });
+
test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => {
const wrapper = mount(
({ eui: euiLightVars, darkMode: false })}>
@@ -192,16 +294,70 @@ describe('PreviewQuery', () => {
);
+ (useMatrixHistogram as jest.Mock).mockReturnValue([
+ false,
+ {
+ inspect: { dsl: [], response: [] },
+ totalCount: 500,
+ refetch: jest.fn(),
+ data: [],
+ buckets: [{ key: 'siem-kibana', doc_count: 500 }],
+ },
+ (useKibana().services.data.search.search as jest.Mock).mockReturnValue(
+ of(getMockEqlResponse())
+ ),
+ ]);
+
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
expect(mockCalls.length).toEqual(1);
+ expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
});
+ test('it renders noise warning when rule type is threshold, and threshold field is defined, timeframe is last hour and hit average is greater than 1/hour', async () => {
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+
+
+ );
+
+ (useMatrixHistogram as jest.Mock).mockReturnValue([
+ false,
+ {
+ inspect: { dsl: [], response: [] },
+ totalCount: 500,
+ refetch: jest.fn(),
+ data: [],
+ buckets: [
+ { key: 'siem-kibana', doc_count: 200 },
+ { key: 'siem-windows', doc_count: 300 },
+ ],
+ },
+ (useKibana().services.data.search.search as jest.Mock).mockReturnValue(
+ of(getMockEqlResponse())
+ ),
+ ]);
+
+ wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
+
+ expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
+ });
+
test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is not defined', () => {
const wrapper = mount(
({ eui: euiLightVars, darkMode: false })}>
@@ -255,4 +411,33 @@ describe('PreviewQuery', () => {
expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
});
+
+ test('it hides histogram when timeframe changes', () => {
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+
+
+ );
+
+ wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
+
+ expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy();
+
+ wrapper
+ .find('[data-test-subj="queryPreviewTimeframeSelect"] select')
+ .at(0)
+ .simulate('change', { target: { value: 'd' } });
+
+ expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy();
+ });
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx
index f1cb8e3ba9fdbe..6669ea6d979692 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, useCallback, useEffect, useReducer } from 'react';
+import React, { Fragment, useCallback, useEffect, useReducer, useRef } from 'react';
import { Unit } from '@elastic/datemath';
import styled from 'styled-components';
import {
@@ -110,29 +110,31 @@ export const PreviewQuery = ({
{ inspect, totalCount: matrixHistTotal, refetch, data: matrixHistoData, buckets },
startNonEql,
] = useMatrixHistogram({
- errorMessage: i18n.PREVIEW_QUERY_ERROR,
+ errorMessage: i18n.QUERY_PREVIEW_ERROR,
endDate: fromTime,
startDate: toTime,
filterQuery: queryFilter,
indexNames: index,
histogramType: MatrixHistogramType.events,
stackByField: 'event.category',
- threshold,
+ threshold: ruleType === 'threshold' ? threshold : undefined,
skip: true,
});
const setQueryInfo = useCallback(
- (queryBar: FieldValueQueryBar | undefined): void => {
+ (queryBar: FieldValueQueryBar | undefined, indices: string[], type: Type): void => {
dispatch({
type: 'setQueryInfo',
queryBar,
- index,
- ruleType,
+ index: indices,
+ ruleType: type,
});
},
- [dispatch, index, ruleType]
+ [dispatch]
);
+ const debouncedSetQueryInfo = useRef(debounce(500, setQueryInfo));
+
const setTimeframeSelect = useCallback(
(selection: Unit): void => {
dispatch({
@@ -190,11 +192,9 @@ export const PreviewQuery = ({
[dispatch]
);
- useEffect((): void => {
- const debounced = debounce(1000, setQueryInfo);
-
- debounced(query);
- }, [setQueryInfo, query]);
+ useEffect(() => {
+ debouncedSetQueryInfo.current(query, index, ruleType);
+ }, [index, query, ruleType]);
useEffect((): void => {
setThresholdValues(threshold, ruleType);
@@ -205,12 +205,32 @@ export const PreviewQuery = ({
}, [ruleType, setRuleTypeChange]);
useEffect((): void => {
- const totalHits = ruleType === 'eql' ? eqlQueryTotal : matrixHistTotal;
-
- if (isNoisy(totalHits, timeframe)) {
- setNoiseWarning();
+ switch (ruleType) {
+ case 'eql':
+ if (isNoisy(eqlQueryTotal, timeframe)) {
+ setNoiseWarning();
+ }
+ break;
+ case 'threshold':
+ const totalHits = thresholdFieldExists ? buckets.length : matrixHistTotal;
+ if (isNoisy(totalHits, timeframe)) {
+ setNoiseWarning();
+ }
+ break;
+ default:
+ if (isNoisy(matrixHistTotal, timeframe)) {
+ setNoiseWarning();
+ }
}
- }, [timeframe, matrixHistTotal, eqlQueryTotal, ruleType, setNoiseWarning]);
+ }, [
+ timeframe,
+ matrixHistTotal,
+ eqlQueryTotal,
+ ruleType,
+ setNoiseWarning,
+ thresholdFieldExists,
+ buckets.length,
+ ]);
const handlePreviewEqlQuery = useCallback(
(to: string, from: string): void => {
@@ -263,8 +283,9 @@ export const PreviewQuery = ({
options={timeframeOptions}
value={timeframe}
onChange={handleSelectPreviewTimeframe}
- aria-label={i18n.PREVIEW_SELECT_ARIA}
+ aria-label={i18n.QUERY_PREVIEW_SELECT_ARIA}
disabled={isDisabled}
+ data-test-subj="queryPreviewTimeframeSelect"
/>
@@ -276,7 +297,7 @@ export const PreviewQuery = ({
onClick={handlePreviewClicked}
data-test-subj="queryPreviewButton"
>
- {i18n.PREVIEW_LABEL}
+ {i18n.QUERY_PREVIEW_BUTTON}
@@ -307,7 +328,6 @@ export const PreviewQuery = ({
{
expect(update).toEqual(initialState);
});
- test('should reset showHistogram and warnings if queryBar undefined', () => {
+ test('should reset showHistogram if queryBar undefined', () => {
const update = reducer(
- { ...initialState, showHistogram: true, warnings: ['uh oh'] },
+ { ...initialState, showHistogram: true },
{
type: 'setQueryInfo',
queryBar: undefined,
@@ -44,11 +44,10 @@ describe('queryPreviewReducer', () => {
}
);
- expect(update.warnings).toEqual([]);
expect(update.showHistogram).toBeFalsy();
});
- test('should reset showHistogram and warnings if queryBar defined', () => {
+ test('should reset showHistogram if queryBar defined', () => {
const update = reducer(
{ ...initialState, showHistogram: true, warnings: ['uh oh'] },
{
@@ -62,7 +61,6 @@ describe('queryPreviewReducer', () => {
}
);
- expect(update.warnings).toEqual([]);
expect(update.showHistogram).toBeFalsy();
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts
index 76047a0af5c541..ba27098a8c350f 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts
@@ -82,13 +82,11 @@ export const queryPreviewReducer = () => (state: State, action: Action): State =
filters,
queryFilter,
showHistogram: false,
- warnings: [],
};
}
return {
...state,
- warnings: [],
showHistogram: false,
};
}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx
index 1021c5b8ddcb70..a102c567a98e88 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx
@@ -56,12 +56,12 @@ export const PreviewThresholdQueryHistogram = ({
};
}, [buckets]);
- const barConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(200), []);
+ const barConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(), []);
const subtitle = useMemo(
(): string =>
isLoading
- ? i18n.PREVIEW_SUBTITLE_LOADING
+ ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING
: i18n.QUERY_PREVIEW_THRESHOLD_WITH_FIELD_TITLE(totalCount),
[isLoading, totalCount]
);
@@ -73,7 +73,7 @@ export const PreviewThresholdQueryHistogram = ({
barConfig={barConfig}
title={i18n.QUERY_GRAPH_HITS_TITLE}
subtitle={subtitle}
- disclaimer={i18n.PREVIEW_QUERY_DISCLAIMER}
+ disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER}
isLoading={isLoading}
data-test-subj="thresholdQueryPreviewHistogram"
/>
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts
index 7ae75c51dcf5a1..0d080113aeae83 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts
@@ -6,14 +6,14 @@
import { i18n } from '@kbn/i18n';
-export const PREVIEW_LABEL = i18n.translate(
- 'xpack.securitySolution.stepDefineRule.previewQueryLabel',
+export const QUERY_PREVIEW_BUTTON = i18n.translate(
+ 'xpack.securitySolution.stepDefineRule.previewQueryButton',
{
defaultMessage: 'Preview results',
}
);
-export const PREVIEW_SELECT_ARIA = i18n.translate(
+export const QUERY_PREVIEW_SELECT_ARIA = i18n.translate(
'xpack.securitySolution.stepDefineRule.previewQueryAriaLabel',
{
defaultMessage: 'Query preview timeframe select',
@@ -85,14 +85,14 @@ export const QUERY_PREVIEW_NO_HITS = i18n.translate(
}
);
-export const PREVIEW_QUERY_ERROR = i18n.translate(
+export const QUERY_PREVIEW_ERROR = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError',
{
defaultMessage: 'Error fetching preview',
}
);
-export const PREVIEW_QUERY_DISCLAIMER = i18n.translate(
+export const QUERY_PREVIEW_DISCLAIMER = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer',
{
defaultMessage:
@@ -100,7 +100,7 @@ export const PREVIEW_QUERY_DISCLAIMER = i18n.translate(
}
);
-export const PREVIEW_QUERY_DISCLAIMER_EQL = i18n.translate(
+export const QUERY_PREVIEW_DISCLAIMER_EQL = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql',
{
defaultMessage:
@@ -108,26 +108,24 @@ export const PREVIEW_QUERY_DISCLAIMER_EQL = i18n.translate(
}
);
-export const PREVIEW_WARNING_CAP_HIT = (cap: number) =>
- i18n.translate(
- 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphCapHitWarning',
- {
- values: { cap },
- defaultMessage:
- 'Hit query cap size of {cap}. This query could produce more hits than the {cap} shown.',
- }
- );
+export const QUERY_PREVIEW_SUBTITLE_LOADING = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewSubtitleLoading',
+ {
+ defaultMessage: '...loading',
+ }
+);
-export const PREVIEW_WARNING_TIMESTAMP = i18n.translate(
- 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTimestampWarning',
+export const QUERY_PREVIEW_EQL_SEQUENCE_TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewEqlSequenceTitle',
{
- defaultMessage: 'Unable to find "@timestamp" field on events.',
+ defaultMessage: 'No histogram available',
}
);
-export const PREVIEW_SUBTITLE_LOADING = i18n.translate(
- 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewSubtitleLoading',
+export const QUERY_PREVIEW_EQL_SEQUENCE_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewEqlSequenceDescription',
{
- defaultMessage: '...loading',
+ defaultMessage:
+ 'No histogram is available at this time for EQL sequence queries. You can use the inspect in the top right corner to view query details.',
}
);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
index fc03e07442f9e8..2479a260872be7 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
@@ -87,10 +87,10 @@ const StepAboutRuleComponent: FC = ({
schema,
});
const { getFields, getFormData, submit } = form;
- const [{ severity: formSeverity }] = (useFormData({
+ const [{ severity: formSeverity }] = useFormData({
form,
watch: ['severity'],
- }) as unknown) as [Partial];
+ });
useEffect(() => {
const formSeverityValue = formSeverity?.value;
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx
index 8e398e6236510a..757319e7aa1ae9 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx
@@ -154,7 +154,9 @@ describe('StepAboutRuleToggleDetails', () => {
.simulate('change', { target: { value: 'notes' } });
expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy();
- expect(wrapper.find('Markdown h1').text()).toEqual('this is some markdown documentation');
+ expect(wrapper.find('.euiMarkdownFormat').text()).toEqual(
+ 'this is some markdown documentation'
+ );
});
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
index 8604a5293a7107..52e9dc7e44ff71 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
@@ -20,7 +20,7 @@ import styled from 'styled-components';
import { isEmpty } from 'lodash/fp';
import { HeaderSection } from '../../../../common/components/header_section';
-import { Markdown } from '../../../../common/components/markdown';
+import { MarkdownRenderer } from '../../../../common/components/markdown_editor';
import { AboutStepRule, AboutStepRuleDetails } from '../../../pages/detection_engine/rules/types';
import * as i18n from './translations';
import { StepAboutRule } from '../step_about_rule';
@@ -136,7 +136,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({
maxHeight={aboutPanelHeight}
className="eui-yScrollWithShadows"
>
-
+ {stepDataDetails.note}
)}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
index 27d69c68870114..8a5966c71aa288 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
@@ -5,7 +5,7 @@
*/
import { EuiButtonEmpty, EuiFormRow, EuiSpacer } from '@elastic/eui';
-import React, { FC, memo, useCallback, useState, useEffect } from 'react';
+import React, { FC, memo, useCallback, useState, useEffect, useMemo } from 'react';
import styled from 'styled-components';
// Prefer importing entire lodash library, e.g. import { get } from "lodash"
// eslint-disable-next-line no-restricted-imports
@@ -53,7 +53,7 @@ import {
import { EqlQueryBar } from '../eql_query_bar';
import { ThreatMatchInput } from '../threatmatch_input';
import { useFetchIndex } from '../../../../common/containers/source';
-import { PreviewQuery } from '../query_preview';
+import { PreviewQuery, Threshold } from '../query_preview';
const CommonUseField = getUseField({ component: Field });
@@ -141,17 +141,15 @@ const StepDefineRuleComponent: FC = ({
'threshold.value': formThresholdValue,
'threshold.field': formThresholdField,
},
- ] = (useFormData({
+ ] = useFormData<
+ DefineStepRule & {
+ 'threshold.value': number | undefined;
+ 'threshold.field': string[] | undefined;
+ }
+ >({
form,
watch: ['index', 'ruleType', 'queryBar', 'threshold.value', 'threshold.field', 'threatIndex'],
- }) as unknown) as [
- Partial<
- DefineStepRule & {
- 'threshold.value': number | undefined;
- 'threshold.field': string[] | undefined;
- }
- >
- ];
+ });
const [isQueryBarValid, setIsQueryBarValid] = useState(false);
const index = formIndex || initialState.index;
const threatIndex = formThreatIndex || initialState.threatIndex;
@@ -212,6 +210,12 @@ const StepDefineRuleComponent: FC = ({
setOpenTimelineSearch(false);
}, []);
+ const thresholdFormValue = useMemo((): Threshold | undefined => {
+ return formThresholdValue != null && formThresholdField != null
+ ? { value: formThresholdValue, field: formThresholdField[0] }
+ : undefined;
+ }, [formThresholdField, formThresholdValue]);
+
const ThresholdInputChildren = useCallback(
({ thresholdField, thresholdValue }) => (
= ({
index={index}
query={formQuery}
isDisabled={queryBarQuery.trim() === '' || !isQueryBarValid || index.length === 0}
- threshold={
- formThresholdValue != null && formThresholdField != null
- ? { value: formThresholdValue, field: formThresholdField[0] }
- : undefined
- }
+ threshold={thresholdFormValue}
/>
>
)}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx
index 349a79d4e40f9c..dd1d92e7e72a3a 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx
@@ -93,10 +93,10 @@ const StepRuleActionsComponent: FC = ({
schema,
});
const { getFields, getFormData, submit } = form;
- const [{ throttle: formThrottle }] = (useFormData({
+ const [{ throttle: formThrottle }] = useFormData({
form,
watch: ['throttle'],
- }) as unknown) as [Partial];
+ });
const throttle = formThrottle || initialState.throttle;
const handleSubmit = useCallback(
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
index b4eb40d0818998..54aae5c41bd5f2 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
@@ -79,7 +79,7 @@ import { ExceptionsViewer } from '../../../../../common/components/exceptions/vi
import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants';
import { useFullScreen } from '../../../../../common/containers/use_full_screen';
import { Display } from '../../../../../hosts/pages/display';
-import { ExceptionListTypeEnum, ExceptionIdentifiers } from '../../../../../shared_imports';
+import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports';
import { isMlRule } from '../../../../../../common/machine_learning/helpers';
import { isThresholdRule } from '../../../../../../common/detection_engine/utils';
import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async';
@@ -354,12 +354,12 @@ export const RuleDetailsPageComponent: FC = ({
const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections);
const exceptionLists = useMemo((): {
- lists: ExceptionIdentifiers[];
+ lists: ExceptionListIdentifiers[];
allowedExceptionListTypes: ExceptionListTypeEnum[];
} => {
if (rule != null && rule.exceptions_list != null) {
return rule.exceptions_list.reduce<{
- lists: ExceptionIdentifiers[];
+ lists: ExceptionListIdentifiers[];
allowedExceptionListTypes: ExceptionListTypeEnum[];
}>(
(acc, { id, list_id: listId, namespace_type: namespaceType, type }) => {
@@ -542,6 +542,7 @@ export const RuleDetailsPageComponent: FC = ({
loading={loading}
showBuildingBlockAlerts={showBuildingBlockAlerts}
onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback}
+ onRuleChange={refreshRule}
to={to}
/>
)}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
index 0eedecef22170c..b76e0c8acf4c35 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
@@ -238,11 +238,23 @@ describe('policy details: ', () => {
security: true,
},
malware: { mode: 'prevent' },
+ popup: {
+ malware: {
+ enabled: true,
+ message: '',
+ },
+ },
logging: { file: 'info' },
},
mac: {
events: { process: true, file: true, network: true },
malware: { mode: 'prevent' },
+ popup: {
+ malware: {
+ enabled: true,
+ message: '',
+ },
+ },
logging: { file: 'info' },
},
linux: {
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts
index c236b2841fc858..953438526b87ed 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts
@@ -105,10 +105,12 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel
windows: {
events: windows.events,
malware: windows.malware,
+ popup: windows.popup,
},
mac: {
events: mac.events,
malware: mac.malware,
+ popup: mac.popup,
},
linux: {
events: linux.events,
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx
index 1698f5bc3fd0c2..1da832fb081efc 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx
@@ -14,6 +14,7 @@ import {
EuiSpacer,
htmlIdGenerator,
EuiCallOut,
+ EuiCheckbox,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -50,6 +51,11 @@ const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label:
const newPayload = clone(policyDetailsConfig);
for (const os of OSes) {
newPayload[os][protection].mode = id;
+ if (id === ProtectionModes.prevent) {
+ newPayload[os].popup[protection].enabled = true;
+ } else {
+ newPayload[os].popup[protection].enabled = false;
+ }
}
dispatch({
type: 'userChangedPolicyConfig',
@@ -85,6 +91,8 @@ export const MalwareProtections = React.memo(() => {
const dispatch = useDispatch();
// currently just taking windows.malware, but both windows.malware and mac.malware should be the same value
const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode;
+ const userNotificationSelected =
+ policyDetailsConfig && policyDetailsConfig.windows.popup.malware.enabled;
const radios: Immutable {
if (event.target.checked === false) {
for (const os of OSes) {
newPayload[os][protection].mode = ProtectionModes.off;
+ newPayload[os].popup[protection].enabled = event.target.checked;
}
} else {
for (const os of OSes) {
newPayload[os][protection].mode = ProtectionModes.prevent;
+ newPayload[os].popup[protection].enabled = event.target.checked;
}
}
dispatch({
@@ -131,6 +141,22 @@ export const MalwareProtections = React.memo(() => {
[dispatch, policyDetailsConfig]
);
+ const handleUserNotificationCheckbox = useCallback(
+ (event) => {
+ if (policyDetailsConfig) {
+ const newPayload = clone(policyDetailsConfig);
+ for (const os of OSes) {
+ newPayload[os].popup[protection].enabled = event.target.checked;
+ }
+ dispatch({
+ type: 'userChangedPolicyConfig',
+ payload: { policyConfig: newPayload },
+ });
+ }
+ },
+ [policyDetailsConfig, dispatch]
+ );
+
const radioButtons = useMemo(() => {
return (
<>
@@ -154,9 +180,27 @@ export const MalwareProtections = React.memo(() => {
);
})}
+
+
+
+
+
+
+
>
);
- }, [radios]);
+ }, [radios, handleUserNotificationCheckbox, userNotificationSelected]);
const protectionSwitch = useMemo(() => {
return (
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
index 399509466e5731..86cb203671ac2b 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
@@ -344,22 +344,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -595,22 +596,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -846,22 +848,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -1097,22 +1100,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -1348,22 +1352,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -1599,22 +1604,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -1850,22 +1856,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -2101,22 +2108,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -2352,22 +2360,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -2603,22 +2612,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -3144,22 +3154,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -3395,22 +3406,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -3646,22 +3658,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -3897,22 +3910,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -4148,22 +4162,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -4399,22 +4414,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -4650,22 +4666,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -4901,22 +4918,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -5152,22 +5170,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -5403,22 +5422,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -5902,22 +5922,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -6153,22 +6174,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -6404,22 +6426,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -6655,22 +6678,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -6906,22 +6930,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -7157,22 +7182,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -7408,22 +7434,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -7659,22 +7686,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -7910,22 +7938,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
@@ -8161,22 +8190,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap
index a56e71e8073cb1..841f9dc81bd8e7 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap
@@ -996,22 +996,23 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
-
-
+
- Remove
+
+ Remove
+
-
-
+
+
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx
index fea605b13d0672..b898f1b3fd7ab3 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx
@@ -4,13 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { memo, useCallback, useMemo } from 'react';
+import React, { FC, memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
EuiButtonEmpty,
+ EuiButtonProps,
+ PropsForButton,
EuiModal,
EuiModalBody,
EuiModalFooter,
@@ -66,6 +68,21 @@ const getTranslations = (entry: Immutable | undefined) => ({
),
});
+const AutoFocusButton: FC> = memo((props) => {
+ const buttonRef = useRef(null);
+ const button = ;
+
+ useEffect(() => {
+ if (buttonRef.current) {
+ buttonRef.current.focus();
+ }
+ }, []);
+
+ return button;
+});
+
+AutoFocusButton.displayName = 'AutoFocusButton';
+
export const TrustedAppDeletionDialog = memo(() => {
const dispatch = useDispatch>();
const isBusy = useTrustedAppsSelector(isDeletionInProgress);
@@ -100,7 +117,7 @@ export const TrustedAppDeletionDialog = memo(() => {
{translations.cancelButton}
- {
data-test-subj={CONFIRM_SUBJ}
>
{translations.confirmButton}
-
+
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx
index 5867a9d859f04c..95f0fbb194ca62 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx
@@ -10,7 +10,7 @@ import styled from 'styled-components';
import { Case } from '../../../cases/containers/types';
import { getCaseDetailsUrl } from '../../../common/components/link_to/redirect_to_case';
-import { Markdown } from '../../../common/components/markdown';
+import { MarkdownRenderer } from '../../../common/components/markdown_editor';
import { useFormatUrl } from '../../../common/components/link_to';
import { IconWithCount } from '../recent_timelines/counts';
import { LinkAnchor } from '../../../common/components/links';
@@ -52,7 +52,7 @@ const RecentCasesComponent = ({ cases }: { cases: Case[] }) => {
{c.description && c.description.length && (
-
+ {c.description}
)}
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 29aa0b111b78a9..ea8c086c9bdc10 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -85,7 +85,13 @@ export class Plugin implements IPlugin, plugins: SetupPlugins): PluginSetup {
- initTelemetry(plugins.usageCollection, APP_ID);
+ initTelemetry(
+ {
+ usageCollection: plugins.usageCollection,
+ telemetryManagementSection: plugins.telemetryManagementSection,
+ },
+ APP_ID
+ );
if (plugins.home) {
plugins.home.featureCatalogue.registerSolution({
diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts
index f60fefc4f6dfa2..a0484ca39c2b56 100644
--- a/x-pack/plugins/security_solution/public/shared_imports.ts
+++ b/x-pack/plugins/security_solution/public/shared_imports.ts
@@ -49,7 +49,7 @@ export {
updateExceptionListItem,
fetchExceptionListById,
addExceptionList,
- ExceptionIdentifiers,
+ ExceptionListIdentifiers,
ExceptionList,
Pagination,
UseExceptionListSuccess,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap
index 718e7ce1d27a5a..53bc76bfeb8e86 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap
@@ -11,13 +11,6 @@ exports[`AddNote renders correctly 1`] = `
noteInputHeight={200}
updateNewNote={[MockFunction]}
/>
-
-
-
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap
index 9bf2b5c65e8294..69e06bc7e0d1bc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap
@@ -3,54 +3,13 @@
exports[`NewNote renders correctly 1`] = `
,
- "id": "note",
- "name": "Note",
- }
- }
- tabs={
- Array [
- Object {
- "content": ,
- "id": "note",
- "name": "Note",
- },
- Object {
- "content":
-
- ,
- "id": "preview",
- "name": "Preview (Markdown)",
- },
- ]
- }
-/>
+>
+
+
`;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx
index 570c0028e0f516..01dfd72a22db1b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx
@@ -74,7 +74,9 @@ describe('AddNote', () => {
test('it renders the contents of the note', () => {
const wrapper = mount( );
- expect(wrapper.find('[data-test-subj="add-a-note"]').first().text()).toEqual(note);
+ expect(
+ wrapper.find('[data-test-subj="add-a-note"] .euiMarkdownEditorDropZone').first().text()
+ ).toEqual(note);
});
test('it invokes associateNote when the Add Note button is clicked', () => {
@@ -131,30 +133,4 @@ describe('AddNote', () => {
expect(updateNote).toBeCalled();
});
-
- test('it does NOT display the markdown formatting hint when a note has NOT been entered', () => {
- const testProps = {
- ...props,
- newNote: '',
- };
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
- 'visibility',
- 'hidden'
- );
- });
-
- test('it displays the markdown formatting hint when a note has been entered', () => {
- const testProps = {
- ...props,
- newNote: 'We should see a formatting hint now',
- };
- const wrapper = mount( );
-
- expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
- 'visibility',
- 'inline'
- );
- });
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx
index 7c211aafdf8c63..6ba62a115917f7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx
@@ -8,7 +8,6 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/e
import React, { useCallback } from 'react';
import styled from 'styled-components';
-import { MarkdownHint } from '../../../../common/components/markdown/markdown_hint';
import {
AssociateNote,
GetNewNoteId,
@@ -64,9 +63,6 @@ export const AddNote = React.memo<{
return (
-
- 0} />
-
{onCancelAddNote != null ? (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.test.tsx
index c85d9b7dca75ca..0377653ae3b646 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.test.tsx
@@ -7,8 +7,6 @@
import { mount, shallow } from 'enzyme';
import React from 'react';
-import * as i18n from '../translations';
-
import { NewNote } from './new_note';
describe('NewNote', () => {
@@ -21,36 +19,11 @@ describe('NewNote', () => {
expect(wrapper).toMatchSnapshot();
});
- test('it renders a tab labeled "Note"', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('button[role="tab"]').first().text()).toEqual(i18n.NOTE);
- });
-
- test('it renders a tab labeled "Preview (Markdown)"', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('button[role="tab"]').at(1).text()).toEqual(i18n.PREVIEW_MARKDOWN);
- });
-
- test('it renders the expected placeholder when a note is NOT provided', () => {
- const wrapper = mount( );
-
- expect(wrapper.find(`textarea[placeholder="${i18n.ADD_A_NOTE}"]`).exists()).toEqual(true);
- });
-
test('it renders a text area containing the contents of a new (raw) note', () => {
const wrapper = mount( );
- expect(wrapper.find('[data-test-subj="add-a-note"]').first().text()).toEqual(note);
- });
-
- test('it renders a markdown preview when the user clicks Preview (Markdown)', () => {
- const wrapper = mount( );
-
- // click the preview tab:
- wrapper.find('button[role="tab"]').at(1).simulate('click');
-
- expect(wrapper.find('[data-test-subj="markdown-root"]').first().text()).toEqual(note);
+ expect(
+ wrapper.find('[data-test-subj="add-a-note"] .euiMarkdownEditorDropZone').first().text()
+ ).toEqual(note);
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx
index a91c5fc4ecdf35..4b51ab5acce694 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx
@@ -4,74 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiPanel, EuiTabbedContent, EuiTextArea } from '@elastic/eui';
+import { EuiFlexItem } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
-import { Markdown } from '../../../../common/components/markdown';
+import { MarkdownEditor } from '../../../../common/components/markdown_editor';
import { UpdateInternalNewNote } from '../helpers';
import * as i18n from '../translations';
-const NewNoteTabs = styled(EuiTabbedContent)`
+const NewNoteTabs = styled(EuiFlexItem)`
width: 100%;
`;
NewNoteTabs.displayName = 'NewNoteTabs';
-const MarkdownContainer = styled(EuiPanel)<{ height: number }>`
- height: ${({ height }) => height}px;
- overflow: auto;
-`;
-
-MarkdownContainer.displayName = 'MarkdownContainer';
-
-const TextArea = styled(EuiTextArea)<{ height: number }>`
- min-height: ${({ height }) => `${height}px`};
- width: 100%;
-`;
-
-TextArea.displayName = 'TextArea';
-
/** An input for entering a new note */
export const NewNote = React.memo<{
noteInputHeight: number;
note: string;
updateNewNote: UpdateInternalNewNote;
}>(({ note, noteInputHeight, updateNewNote }) => {
- const tabs = [
- {
- id: 'note',
- name: i18n.NOTE,
- content: (
-
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
index 3b6585013c8d36..c6d4325f00739f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
@@ -54,6 +54,7 @@ interface Props {
onRowSelected: OnRowSelected;
onUnPinEvent: OnUnPinEvent;
refetch: inputsModel.Refetch;
+ onRuleChange?: () => void;
selectedEventIds: Readonly>;
showCheckboxes: boolean;
showNotes: boolean;
@@ -88,6 +89,7 @@ export const EventColumnView = React.memo(
onRowSelected,
onUnPinEvent,
refetch,
+ onRuleChange,
selectedEventIds,
showCheckboxes,
showNotes,
@@ -157,6 +159,7 @@ export const EventColumnView = React.memo(
timelineId={timelineId}
disabled={eventType !== 'signal'}
refetch={refetch}
+ onRuleChange={onRuleChange}
/>,
],
[
@@ -171,6 +174,7 @@ export const EventColumnView = React.memo(
isEventPinned,
isEventViewer,
refetch,
+ onRuleChange,
showNotes,
status,
timelineId,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx
index 0fc097de680e8c..752e0bdf25230b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx
@@ -49,6 +49,7 @@ interface Props {
onUnPinEvent: OnUnPinEvent;
pinnedEventIds: Readonly>;
refetch: inputsModel.Refetch;
+ onRuleChange?: () => void;
rowRenderers: RowRenderer[];
selectedEventIds: Readonly>;
showCheckboxes: boolean;
@@ -77,6 +78,7 @@ const EventsComponent: React.FC = ({
onUnPinEvent,
pinnedEventIds,
refetch,
+ onRuleChange,
rowRenderers,
selectedEventIds,
showCheckboxes,
@@ -108,6 +110,7 @@ const EventsComponent: React.FC = ({
onUpdateColumns={onUpdateColumns}
refetch={refetch}
rowRenderers={rowRenderers}
+ onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
showCheckboxes={showCheckboxes}
timelineId={id}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
index be8ce5e26b3e11..4f385a46564833 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
@@ -60,6 +60,7 @@ interface Props {
onUpdateColumns: OnUpdateColumns;
isEventPinned: boolean;
refetch: inputsModel.Refetch;
+ onRuleChange?: () => void;
rowRenderers: RowRenderer[];
selectedEventIds: Readonly>;
showCheckboxes: boolean;
@@ -129,6 +130,7 @@ const StatefulEventComponent: React.FC = ({
onUnPinEvent,
onUpdateColumns,
refetch,
+ onRuleChange,
rowRenderers,
selectedEventIds,
showCheckboxes,
@@ -208,6 +210,7 @@ const StatefulEventComponent: React.FC = ({
onRowSelected={onRowSelected}
onUnPinEvent={onUnPinEvent}
refetch={refetch}
+ onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
showCheckboxes={showCheckboxes}
showNotes={!!showNotes[event._id]}
@@ -296,6 +299,7 @@ const StatefulEventComponent: React.FC = ({
onUnPinEvent,
onUpdateColumns,
refetch,
+ onRuleChange,
rowRenderers,
selectedEventIds,
showCheckboxes,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
index 83b8b119faaecc..e1667ab949732d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
@@ -57,6 +57,7 @@ export interface BodyProps {
onUnPinEvent: OnUnPinEvent;
pinnedEventIds: Readonly>;
refetch: inputsModel.Refetch;
+ onRuleChange?: () => void;
rowRenderers: RowRenderer[];
selectedEventIds: Readonly>;
show: boolean;
@@ -101,6 +102,7 @@ export const Body = React.memo(
pinnedEventIds,
rowRenderers,
refetch,
+ onRuleChange,
selectedEventIds,
show,
showCheckboxes,
@@ -186,6 +188,7 @@ export const Body = React.memo(
pinnedEventIds={pinnedEventIds}
refetch={refetch}
rowRenderers={rowRenderers}
+ onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
showCheckboxes={showCheckboxes}
toggleColumn={toggleColumn}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx
index dfd646353c2750..d7a05e39e76b2f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx
@@ -46,6 +46,7 @@ interface OwnProps {
sort: Sort;
toggleColumn: (column: ColumnHeaderOptions) => void;
refetch: inputsModel.Refetch;
+ onRuleChange?: () => void;
}
type StatefulBodyComponentProps = OwnProps & PropsFromRedux;
@@ -73,6 +74,7 @@ const StatefulBodyComponent = React.memo(
selectedEventIds,
setSelected,
clearSelected,
+ onRuleChange,
show,
showCheckboxes,
graphEventId,
@@ -211,6 +213,7 @@ const StatefulBodyComponent = React.memo(
onUpdateColumns={onUpdateColumns}
pinnedEventIds={pinnedEventIds}
refetch={refetch}
+ onRuleChange={onRuleChange}
rowRenderers={enabledRowRenderers}
selectedEventIds={selectedEventIds}
show={id === TimelineId.active ? show : true}
diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts
index 80cc014285aec6..d72a8b92615872 100644
--- a/x-pack/plugins/security_solution/public/types.ts
+++ b/x-pack/plugins/security_solution/public/types.ts
@@ -13,6 +13,7 @@ import { NewsfeedPublicPluginStart } from '../../../../src/plugins/newsfeed/publ
import { Start as InspectorStart } from '../../../../src/plugins/inspector/public';
import { UiActionsStart } from '../../../../src/plugins/ui_actions/public';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
+import { TelemetryManagementSectionPluginSetup } from '../../../../src/plugins/telemetry_management_section/public';
import { Storage } from '../../../../src/plugins/kibana_utils/public';
import { IngestManagerStart } from '../../ingest_manager/public';
import { PluginStart as ListsPluginStart } from '../../lists/public';
@@ -38,6 +39,7 @@ export interface SetupPlugins {
security: SecurityPluginSetup;
triggersActionsUi: TriggersActionsSetup;
usageCollection?: UsageCollectionSetup;
+ telemetryManagementSection?: TelemetryManagementSectionPluginSetup;
ml?: MlPluginSetup;
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts
index 02e57a71dcd945..0d78c90735ab34 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts
@@ -39,7 +39,6 @@ export class ManifestTask {
setupContract.taskManager.registerTaskDefinitions({
[ManifestTaskConstants.TYPE]: {
title: 'Security Solution Endpoint Exceptions Handler',
- type: ManifestTaskConstants.TYPE,
timeout: ManifestTaskConstants.TIMEOUT,
createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => {
return {
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index aa9d0f1ec6cac6..f5e1c6936cbd6f 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -190,9 +190,7 @@ export class Plugin implements IPlugin {
{
id: 'feature-3',
name: 'Feature 3',
- icon: 'spacesApp',
app: [],
category: DEFAULT_APP_CATEGORIES.management,
privileges: null,
diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx
index 66f5ea87551d3f..270cf862ccbd98 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx
@@ -37,7 +37,6 @@ featuresStart.getFeatures.mockResolvedValue([
new KibanaFeature({
id: 'feature-1',
name: 'feature 1',
- icon: 'spacesApp',
app: [],
category: DEFAULT_APP_CATEGORIES.kibana,
privileges: null,
diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx
index c1d19eb06c2e75..34942221e53eeb 100644
--- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx
+++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx
@@ -45,7 +45,6 @@ featuresStart.getFeatures.mockResolvedValue([
new KibanaFeature({
id: 'feature-1',
name: 'feature 1',
- icon: 'spacesApp',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts
index bf0b51b7e25039..0dd070e63ba31c 100644
--- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts
+++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts
@@ -22,7 +22,6 @@ const features = ([
{
id: 'feature_2',
name: 'Feature 2',
- navLinkId: 'feature2',
app: ['feature2'],
catalogue: ['feature2Entry'],
management: {
@@ -42,7 +41,6 @@ const features = ([
{
id: 'feature_3',
name: 'Feature 3',
- navLinkId: 'feature3',
app: ['feature3_app'],
catalogue: ['feature3Entry'],
management: {
@@ -63,7 +61,6 @@ const features = ([
// feature 4 intentionally delcares the same items as feature 3
id: 'feature_4',
name: 'Feature 4',
- navLinkId: 'feature3',
app: ['feature3', 'feature3_app'],
catalogue: ['feature3Entry'],
management: {
diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts
index 8e22f5a7437e4a..4ac0bc43adcd75 100644
--- a/x-pack/plugins/stack_alerts/server/feature.ts
+++ b/x-pack/plugins/stack_alerts/server/feature.ts
@@ -15,7 +15,6 @@ export const BUILT_IN_ALERTS_FEATURE = {
name: i18n.translate('xpack.stackAlerts.featureRegistry.actionsFeatureName', {
defaultMessage: 'Stack Alerts',
}),
- icon: 'bell',
app: [],
category: DEFAULT_APP_CATEGORIES.management,
management: {
diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts
index 443c8114690021..11f6ccc8818500 100644
--- a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts
+++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts
@@ -5,53 +5,47 @@
*/
import sinon from 'sinon';
-import { mockLogger } from '../test_utils';
-import { TaskManager } from '../task_manager';
import { savedObjectsRepositoryMock } from '../../../../../src/core/server/mocks';
-import {
- SavedObjectsSerializer,
- SavedObjectTypeRegistry,
- SavedObjectsErrorHelpers,
-} from '../../../../../src/core/server';
+import { SavedObjectsErrorHelpers, Logger } from '../../../../../src/core/server';
import { ADJUST_THROUGHPUT_INTERVAL } from '../lib/create_managed_configuration';
+import { TaskManagerPlugin, TaskManagerStartContract } from '../plugin';
+import { coreMock } from '../../../../../src/core/server/mocks';
+import { TaskManagerConfig } from '../config';
describe('managed configuration', () => {
- let taskManager: TaskManager;
+ let taskManagerStart: TaskManagerStartContract;
+ let logger: Logger;
+
let clock: sinon.SinonFakeTimers;
- const callAsInternalUser = jest.fn();
- const logger = mockLogger();
- const serializer = new SavedObjectsSerializer(new SavedObjectTypeRegistry());
const savedObjectsClient = savedObjectsRepositoryMock.create();
- const config = {
- enabled: true,
- max_workers: 10,
- index: 'foo',
- max_attempts: 9,
- poll_interval: 3000,
- max_poll_inactivity_cycles: 10,
- request_capacity: 1000,
- };
- beforeEach(() => {
+ beforeEach(async () => {
jest.resetAllMocks();
- callAsInternalUser.mockResolvedValue({ total: 0, updated: 0, version_conflicts: 0 });
clock = sinon.useFakeTimers();
- taskManager = new TaskManager({
- config,
- logger,
- serializer,
- callAsInternalUser,
- taskManagerId: 'some-uuid',
- savedObjectsRepository: savedObjectsClient,
+
+ const context = coreMock.createPluginInitializerContext({
+ enabled: true,
+ max_workers: 10,
+ index: 'foo',
+ max_attempts: 9,
+ poll_interval: 3000,
+ max_poll_inactivity_cycles: 10,
+ request_capacity: 1000,
});
- taskManager.registerTaskDefinitions({
+ logger = context.logger.get('taskManager');
+
+ const taskManager = new TaskManagerPlugin(context);
+ (await taskManager.setup(coreMock.createSetup())).registerTaskDefinitions({
foo: {
- type: 'foo',
title: 'Foo',
createTaskRunner: jest.fn(),
},
});
- taskManager.start();
+
+ const coreStart = coreMock.createStart();
+ coreStart.savedObjects.createInternalRepository.mockReturnValue(savedObjectsClient);
+ taskManagerStart = await taskManager.start(coreStart);
+
// force rxjs timers to fire when they are scheduled for setTimeout(0) as the
// sinon fake timers cause them to stall
clock.tick(0);
@@ -63,15 +57,17 @@ describe('managed configuration', () => {
savedObjectsClient.create.mockRejectedValueOnce(
SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')
);
+
// Cause "too many requests" error to be thrown
await expect(
- taskManager.schedule({
+ taskManagerStart.schedule({
taskType: 'foo',
state: {},
params: {},
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"Too Many Requests"`);
clock.tick(ADJUST_THROUGHPUT_INTERVAL);
+
expect(logger.warn).toHaveBeenCalledWith(
'Max workers configuration is temporarily reduced after Elasticsearch returned 1 "too many request" error(s).'
);
@@ -85,15 +81,17 @@ describe('managed configuration', () => {
savedObjectsClient.create.mockRejectedValueOnce(
SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')
);
+
// Cause "too many requests" error to be thrown
await expect(
- taskManager.schedule({
+ taskManagerStart.schedule({
taskType: 'foo',
state: {},
params: {},
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"Too Many Requests"`);
clock.tick(ADJUST_THROUGHPUT_INTERVAL);
+
expect(logger.warn).toHaveBeenCalledWith(
'Poll interval configuration is temporarily increased after Elasticsearch returned 1 "too many request" error(s).'
);
diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts
index c007b323384965..d6d776f970a329 100644
--- a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts
+++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts
@@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { mockLogger } from '../test_utils';
+
import { createBuffer, Entity, OperationError, BulkOperation } from './bulk_operation_buffer';
import { mapErr, asOk, asErr, Ok, Err } from './result_type';
-import { mockLogger } from '../test_utils';
interface TaskInstance extends Entity {
attempts: number;
diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts
index 57a14c2f8a56b9..6df5b064f2792e 100644
--- a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts
+++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts
@@ -8,7 +8,7 @@ import { map } from 'lodash';
import { Subject, race, from } from 'rxjs';
import { bufferWhen, filter, bufferCount, flatMap, mapTo, first } from 'rxjs/operators';
import { either, Result, asOk, asErr, Ok, Err } from './result_type';
-import { Logger } from '../types';
+import { Logger } from '../../../../../src/core/server';
export interface BufferOptions {
bufferMaxDuration?: number;
diff --git a/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.test.ts b/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.test.ts
index 408e8d36d3491e..8c81e9b9c5b0a7 100644
--- a/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.test.ts
+++ b/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.test.ts
@@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ensureDeprecatedFieldsAreCorrected } from './correct_deprecated_fields';
import { mockLogger } from '../test_utils';
+import { ensureDeprecatedFieldsAreCorrected } from './correct_deprecated_fields';
describe('ensureDeprecatedFieldsAreCorrected', () => {
test('doesnt change tasks without any schedule fields', async () => {
diff --git a/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.ts b/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.ts
index 2de95cbb8c2fa5..9e5f4b7c143a2c 100644
--- a/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.ts
+++ b/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.ts
@@ -5,7 +5,7 @@
*/
import { TaskInstance, TaskInstanceWithDeprecatedFields } from '../task';
-import { Logger } from '../types';
+import { Logger } from '../../../../../src/core/server';
export function ensureDeprecatedFieldsAreCorrected(
{ id, taskType, interval, schedule, ...taskInstance }: TaskInstanceWithDeprecatedFields,
diff --git a/x-pack/plugins/task_manager/server/lib/create_managed_configuration.test.ts b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.test.ts
index b6b5cd003c5d46..6e1fc71f144a2d 100644
--- a/x-pack/plugins/task_manager/server/lib/create_managed_configuration.test.ts
+++ b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.test.ts
@@ -6,12 +6,12 @@
import sinon from 'sinon';
import { Subject } from 'rxjs';
-import { mockLogger } from '../test_utils';
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
import {
createManagedConfiguration,
ADJUST_THROUGHPUT_INTERVAL,
} from './create_managed_configuration';
+import { mockLogger } from '../test_utils';
describe('createManagedConfiguration()', () => {
let clock: sinon.SinonFakeTimers;
diff --git a/x-pack/plugins/task_manager/server/lib/create_managed_configuration.ts b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.ts
index 3dc5fd50d3ca4c..9d093ec0c671f2 100644
--- a/x-pack/plugins/task_manager/server/lib/create_managed_configuration.ts
+++ b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.ts
@@ -7,7 +7,7 @@
import { interval, merge, of, Observable } from 'rxjs';
import { filter, mergeScan, map, scan, distinctUntilChanged, startWith } from 'rxjs/operators';
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
-import { Logger } from '../types';
+import { Logger } from '../../../../../src/core/server';
const FLUSH_MARKER = Symbol('flush');
export const ADJUST_THROUGHPUT_INTERVAL = 10 * 1000;
@@ -31,7 +31,7 @@ interface ManagedConfigurationOpts {
errors$: Observable;
}
-interface ManagedConfiguration {
+export interface ManagedConfiguration {
maxWorkersConfiguration$: Observable;
pollIntervalConfiguration$: Observable;
}
diff --git a/x-pack/plugins/task_manager/server/lib/middleware.ts b/x-pack/plugins/task_manager/server/lib/middleware.ts
index d367c8ca56c090..c255ddd4775fc3 100644
--- a/x-pack/plugins/task_manager/server/lib/middleware.ts
+++ b/x-pack/plugins/task_manager/server/lib/middleware.ts
@@ -6,49 +6,37 @@
import { RunContext, TaskInstance } from '../task';
-/*
- * BeforeSaveMiddlewareParams is nearly identical to RunContext, but
- * taskInstance is before save (no _id property)
- *
- * taskInstance property is guaranteed to exist. The params can optionally
- * include fields from an "options" object passed as the 2nd parameter to
- * taskManager.schedule()
- */
-export interface BeforeSaveMiddlewareParams {
+type Mapper = (params: T) => Promise;
+interface BeforeSaveContext {
taskInstance: TaskInstance;
}
-export type BeforeSaveFunction = (
- params: BeforeSaveMiddlewareParams
-) => Promise;
-
-export type BeforeRunFunction = (params: RunContext) => Promise;
-export type BeforeMarkRunningFunction = (params: RunContext) => Promise;
+export type BeforeSaveContextFunction = Mapper;
+export type BeforeRunContextFunction = Mapper;
export interface Middleware {
- beforeSave: BeforeSaveFunction;
- beforeRun: BeforeRunFunction;
- beforeMarkRunning: BeforeMarkRunningFunction;
+ beforeSave: BeforeSaveContextFunction;
+ beforeRun: BeforeRunContextFunction;
+ beforeMarkRunning: BeforeRunContextFunction;
}
-export function addMiddlewareToChain(prevMiddleware: Middleware, middleware: Middleware) {
- const beforeSave = middleware.beforeSave
- ? (params: BeforeSaveMiddlewareParams) =>
- middleware.beforeSave(params).then(prevMiddleware.beforeSave)
- : prevMiddleware.beforeSave;
-
- const beforeRun = middleware.beforeRun
- ? (params: RunContext) => middleware.beforeRun(params).then(prevMiddleware.beforeRun)
- : prevMiddleware.beforeRun;
+export function addMiddlewareToChain(prev: Middleware, next: Partial) {
+ return {
+ beforeSave: next.beforeSave ? chain(prev.beforeSave, next.beforeSave) : prev.beforeSave,
+ beforeRun: next.beforeRun ? chain(prev.beforeRun, next.beforeRun) : prev.beforeRun,
+ beforeMarkRunning: next.beforeMarkRunning
+ ? chain(prev.beforeMarkRunning, next.beforeMarkRunning)
+ : prev.beforeMarkRunning,
+ };
+}
- const beforeMarkRunning = middleware.beforeMarkRunning
- ? (params: RunContext) =>
- middleware.beforeMarkRunning(params).then(prevMiddleware.beforeMarkRunning)
- : prevMiddleware.beforeMarkRunning;
+const chain = (prev: Mapper, next: Mapper): Mapper => (params) =>
+ next(params).then(prev);
+export function createInitialMiddleware(): Middleware {
return {
- beforeSave,
- beforeRun,
- beforeMarkRunning,
+ beforeSave: async (saveOpts: BeforeSaveContext) => saveOpts,
+ beforeRun: async (runOpts: RunContext) => runOpts,
+ beforeMarkRunning: async (runOpts: RunContext) => runOpts,
};
}
diff --git a/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.ts b/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.ts
deleted file mode 100644
index f5856aa6fac334..00000000000000
--- a/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import Joi from 'joi';
-import { TaskDefinition, TaskDictionary, validateTaskDefinition } from '../task';
-
-/**
- * Sanitizes the system's task definitions. Task definitions have optional properties, and
- * this ensures they all are given a reasonable default.
- *
- * @param taskDefinitions - The Kibana task definitions dictionary
- */
-export function sanitizeTaskDefinitions(
- taskDefinitions: TaskDictionary = {}
-): TaskDictionary {
- return Object.keys(taskDefinitions).reduce((acc, type) => {
- const rawDefinition = taskDefinitions[type];
- rawDefinition.type = type;
- acc[type] = Joi.attempt(rawDefinition, validateTaskDefinition) as TaskDefinition;
- return acc;
- }, {} as TaskDictionary);
-}
diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts
new file mode 100644
index 00000000000000..50e7e9a7aa1971
--- /dev/null
+++ b/x-pack/plugins/task_manager/server/plugin.test.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TaskManagerPlugin } from './plugin';
+import { coreMock } from '../../../../src/core/server/mocks';
+import { TaskManagerConfig } from './config';
+
+describe('TaskManagerPlugin', () => {
+ describe('setup', () => {
+ test('throws if no valid UUID is available', async () => {
+ const pluginInitializerContext = coreMock.createPluginInitializerContext({
+ enabled: true,
+ max_workers: 10,
+ index: 'foo',
+ max_attempts: 9,
+ poll_interval: 3000,
+ max_poll_inactivity_cycles: 10,
+ request_capacity: 1000,
+ });
+
+ pluginInitializerContext.env.instanceUuid = '';
+
+ const taskManagerPlugin = new TaskManagerPlugin(pluginInitializerContext);
+ expect(taskManagerPlugin.setup(coreMock.createSetup())).rejects.toEqual(
+ new Error(`TaskManager is unable to start as Kibana has no valid UUID assigned to it.`)
+ );
+ });
+
+ test('throws if setup methods are called after start', async () => {
+ const pluginInitializerContext = coreMock.createPluginInitializerContext({
+ enabled: true,
+ max_workers: 10,
+ index: 'foo',
+ max_attempts: 9,
+ poll_interval: 3000,
+ max_poll_inactivity_cycles: 10,
+ request_capacity: 1000,
+ });
+
+ const taskManagerPlugin = new TaskManagerPlugin(pluginInitializerContext);
+
+ const setupApi = await taskManagerPlugin.setup(coreMock.createSetup());
+
+ await taskManagerPlugin.start(coreMock.createStart());
+
+ expect(() =>
+ setupApi.addMiddleware({
+ beforeSave: async (saveOpts) => saveOpts,
+ beforeRun: async (runOpts) => runOpts,
+ beforeMarkRunning: async (runOpts) => runOpts,
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Cannot add Middleware after the task manager has started"`
+ );
+
+ expect(() =>
+ setupApi.registerTaskDefinitions({
+ lateRegisteredType: {
+ title: 'lateRegisteredType',
+ createTaskRunner: () => ({ async run() {} }),
+ },
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Cannot register task definitions after the task manager has started"`
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts
index d7dcf779376bff..0381698e6fb770 100644
--- a/x-pack/plugins/task_manager/server/plugin.ts
+++ b/x-pack/plugins/task_manager/server/plugin.ts
@@ -3,92 +3,140 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { PluginInitializerContext, Plugin, CoreSetup, CoreStart } from 'src/core/server';
-import { Subject } from 'rxjs';
+import { PluginInitializerContext, Plugin, CoreSetup, Logger, CoreStart } from 'src/core/server';
import { first } from 'rxjs/operators';
-import { TaskDictionary, TaskDefinition } from './task';
-import { TaskManager } from './task_manager';
+import { TaskDefinition } from './task';
+import { TaskPollingLifecycle } from './polling_lifecycle';
import { TaskManagerConfig } from './config';
-import { Middleware } from './lib/middleware';
+import { createInitialMiddleware, addMiddlewareToChain, Middleware } from './lib/middleware';
import { setupSavedObjects } from './saved_objects';
+import { TaskTypeDictionary } from './task_type_dictionary';
+import { FetchResult, SearchOpts, TaskStore } from './task_store';
+import { createManagedConfiguration } from './lib/create_managed_configuration';
+import { TaskScheduling } from './task_scheduling';
-export type TaskManagerSetupContract = Pick<
- TaskManager,
- 'addMiddleware' | 'registerTaskDefinitions'
+export type TaskManagerSetupContract = { addMiddleware: (middleware: Middleware) => void } & Pick<
+ TaskTypeDictionary,
+ 'registerTaskDefinitions'
>;
export type TaskManagerStartContract = Pick<
- TaskManager,
- 'fetch' | 'get' | 'remove' | 'schedule' | 'runNow' | 'ensureScheduled'
->;
+ TaskScheduling,
+ 'schedule' | 'runNow' | 'ensureScheduled'
+> &
+ Pick;
export class TaskManagerPlugin
implements Plugin {
- legacyTaskManager$: Subject = new Subject();
- taskManager: Promise = this.legacyTaskManager$.pipe(first()).toPromise();
- currentConfig: TaskManagerConfig;
- taskManagerId?: string;
- config?: TaskManagerConfig;
+ private taskPollingLifecycle?: TaskPollingLifecycle;
+ private taskManagerId?: string;
+ private config?: TaskManagerConfig;
+ private logger: Logger;
+ private definitions: TaskTypeDictionary;
+ private middleware: Middleware = createInitialMiddleware();
constructor(private readonly initContext: PluginInitializerContext) {
this.initContext = initContext;
- this.currentConfig = {} as TaskManagerConfig;
+ this.logger = initContext.logger.get();
+ this.definitions = new TaskTypeDictionary(this.logger);
}
- public async setup(core: CoreSetup): Promise {
+ public async setup({ savedObjects }: CoreSetup): Promise {
this.config = await this.initContext.config
.create()
.pipe(first())
.toPromise();
- setupSavedObjects(core.savedObjects, this.config);
+ setupSavedObjects(savedObjects, this.config);
this.taskManagerId = this.initContext.env.instanceUuid;
+ if (!this.taskManagerId) {
+ this.logger.error(
+ `TaskManager is unable to start as there the Kibana UUID is invalid (value of the "server.uuid" configuration is ${this.taskManagerId})`
+ );
+ throw new Error(`TaskManager is unable to start as Kibana has no valid UUID assigned to it.`);
+ } else {
+ this.logger.info(`TaskManager is identified by the Kibana UUID: ${this.taskManagerId}`);
+ }
+
return {
addMiddleware: (middleware: Middleware) => {
- this.taskManager.then((tm) => tm.addMiddleware(middleware));
+ this.assertStillInSetup('add Middleware');
+ this.middleware = addMiddlewareToChain(this.middleware, middleware);
},
- registerTaskDefinitions: (taskDefinition: TaskDictionary) => {
- this.taskManager.then((tm) => tm.registerTaskDefinitions(taskDefinition));
+ registerTaskDefinitions: (taskDefinition: Record) => {
+ this.assertStillInSetup('register task definitions');
+ this.definitions.registerTaskDefinitions(taskDefinition);
},
};
}
public start({ savedObjects, elasticsearch }: CoreStart): TaskManagerStartContract {
- const logger = this.initContext.logger.get('taskManager');
const savedObjectsRepository = savedObjects.createInternalRepository(['task']);
- this.legacyTaskManager$.next(
- new TaskManager({
- taskManagerId: this.taskManagerId!,
- config: this.config!,
- savedObjectsRepository,
- serializer: savedObjects.createSerializer(),
- callAsInternalUser: elasticsearch.legacy.client.callAsInternalUser,
- logger,
- })
- );
- this.legacyTaskManager$.complete();
-
- // we need to "drain" any calls made to the seup API
- // before `starting` TaskManager. This is a legacy relic
- // of the old API that should be resolved once we split
- // Task manager into two services, setup and start, instead
- // of the single instance of TaskManager
- this.taskManager.then((tm) => tm.start());
+ const taskStore = new TaskStore({
+ serializer: savedObjects.createSerializer(),
+ savedObjectsRepository,
+ esClient: elasticsearch.createClient('taskManager').asInternalUser,
+ index: this.config!.index,
+ maxAttempts: this.config!.max_attempts,
+ definitions: this.definitions,
+ taskManagerId: `kibana:${this.taskManagerId!}`,
+ });
+
+ const { maxWorkersConfiguration$, pollIntervalConfiguration$ } = createManagedConfiguration({
+ logger: this.logger,
+ errors$: taskStore.errors$,
+ startingMaxWorkers: this.config!.max_workers,
+ startingPollInterval: this.config!.poll_interval,
+ });
+
+ const taskPollingLifecycle = new TaskPollingLifecycle({
+ config: this.config!,
+ definitions: this.definitions,
+ logger: this.logger,
+ taskStore,
+ middleware: this.middleware,
+ maxWorkersConfiguration$,
+ pollIntervalConfiguration$,
+ });
+ this.taskPollingLifecycle = taskPollingLifecycle;
+
+ const taskScheduling = new TaskScheduling({
+ logger: this.logger,
+ taskStore,
+ middleware: this.middleware,
+ taskPollingLifecycle,
+ });
+
+ // start polling for work
+ taskPollingLifecycle.start();
return {
- fetch: (...args) => this.taskManager.then((tm) => tm.fetch(...args)),
- get: (...args) => this.taskManager.then((tm) => tm.get(...args)),
- remove: (...args) => this.taskManager.then((tm) => tm.remove(...args)),
- schedule: (...args) => this.taskManager.then((tm) => tm.schedule(...args)),
- runNow: (...args) => this.taskManager.then((tm) => tm.runNow(...args)),
- ensureScheduled: (...args) => this.taskManager.then((tm) => tm.ensureScheduled(...args)),
+ fetch: (opts: SearchOpts): Promise => taskStore.fetch(opts),
+ get: (id: string) => taskStore.get(id),
+ remove: (id: string) => taskStore.remove(id),
+ schedule: (...args) => taskScheduling.schedule(...args),
+ ensureScheduled: (...args) => taskScheduling.ensureScheduled(...args),
+ runNow: (...args) => taskScheduling.runNow(...args),
};
}
+
public stop() {
- this.taskManager.then((tm) => {
- tm.stop();
- });
+ if (this.taskPollingLifecycle) {
+ this.taskPollingLifecycle.stop();
+ }
+ }
+
+ /**
+ * Ensures task manager hasn't started
+ *
+ * @param {string} the name of the operation being executed
+ * @returns void
+ */
+ private assertStillInSetup(operation: string) {
+ if (this.taskPollingLifecycle?.isStarted) {
+ throw new Error(`Cannot ${operation} after the task manager has started`);
+ }
}
}
diff --git a/x-pack/plugins/task_manager/server/polling/task_poller.ts b/x-pack/plugins/task_manager/server/polling/task_poller.ts
index 7515668a19d403..3d48453aa5a9af 100644
--- a/x-pack/plugins/task_manager/server/polling/task_poller.ts
+++ b/x-pack/plugins/task_manager/server/polling/task_poller.ts
@@ -15,7 +15,7 @@ import { mapTo, filter, scan, concatMap, tap, catchError, switchMap } from 'rxjs
import { pipe } from 'fp-ts/lib/pipeable';
import { Option, none, map as mapOptional, getOrElse } from 'fp-ts/lib/Option';
-import { Logger } from '../types';
+import { Logger } from '../../../../../src/core/server';
import { pullFromSet } from '../lib/pull_from_set';
import {
Result,
diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.mock.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.mock.ts
new file mode 100644
index 00000000000000..9df1e06165bc61
--- /dev/null
+++ b/x-pack/plugins/task_manager/server/polling_lifecycle.mock.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TaskPollingLifecycle, TaskLifecycleEvent } from './polling_lifecycle';
+import { of, Observable } from 'rxjs';
+
+export const taskPollingLifecycleMock = {
+ create(opts: { isStarted?: boolean; events$?: Observable }) {
+ return ({
+ start: jest.fn(),
+ attemptToRun: jest.fn(),
+ get isStarted() {
+ return opts.isStarted ?? true;
+ },
+ get events() {
+ return opts.events$ ?? of();
+ },
+ stop: jest.fn(),
+ } as unknown) as jest.Mocked;
+ },
+};
diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts
new file mode 100644
index 00000000000000..29c8e836303f8c
--- /dev/null
+++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import _ from 'lodash';
+import sinon from 'sinon';
+import { of } from 'rxjs';
+
+import { TaskPollingLifecycle, claimAvailableTasks } from './polling_lifecycle';
+import { createInitialMiddleware } from './lib/middleware';
+import { TaskTypeDictionary } from './task_type_dictionary';
+import { taskStoreMock } from './task_store.mock';
+import { mockLogger } from './test_utils';
+
+describe('TaskPollingLifecycle', () => {
+ let clock: sinon.SinonFakeTimers;
+
+ const taskManagerLogger = mockLogger();
+ const mockTaskStore = taskStoreMock.create({});
+ const taskManagerOpts = {
+ config: {
+ enabled: true,
+ max_workers: 10,
+ index: 'foo',
+ max_attempts: 9,
+ poll_interval: 6000000,
+ max_poll_inactivity_cycles: 10,
+ request_capacity: 1000,
+ },
+ taskStore: mockTaskStore,
+ logger: taskManagerLogger,
+ definitions: new TaskTypeDictionary(taskManagerLogger),
+ middleware: createInitialMiddleware(),
+ maxWorkersConfiguration$: of(100),
+ pollIntervalConfiguration$: of(100),
+ };
+
+ beforeEach(() => {
+ clock = sinon.useFakeTimers();
+ taskManagerOpts.definitions = new TaskTypeDictionary(taskManagerLogger);
+ });
+
+ afterEach(() => clock.restore());
+
+ describe('start', () => {
+ test('begins polling once start is called', () => {
+ const taskManager = new TaskPollingLifecycle(taskManagerOpts);
+
+ clock.tick(150);
+ expect(mockTaskStore.claimAvailableTasks).not.toHaveBeenCalled();
+
+ taskManager.start();
+
+ clock.tick(150);
+ expect(mockTaskStore.claimAvailableTasks).toHaveBeenCalled();
+ });
+ });
+
+ describe('claimAvailableTasks', () => {
+ test('should claim Available Tasks when there are available workers', () => {
+ const logger = mockLogger();
+ const claim = jest.fn(() => Promise.resolve({ docs: [], claimedTasks: 0 }));
+
+ const availableWorkers = 1;
+
+ claimAvailableTasks([], claim, availableWorkers, logger);
+
+ expect(claim).toHaveBeenCalledTimes(1);
+ });
+
+ test('should not claim Available Tasks when there are no available workers', () => {
+ const logger = mockLogger();
+ const claim = jest.fn(() => Promise.resolve({ docs: [], claimedTasks: 0 }));
+
+ const availableWorkers = 0;
+
+ claimAvailableTasks([], claim, availableWorkers, logger);
+
+ expect(claim).not.toHaveBeenCalled();
+ });
+
+ /**
+ * This handles the case in which Elasticsearch has had inline script disabled.
+ * This is achieved by setting the `script.allowed_types` flag on Elasticsearch to `none`
+ */
+ test('handles failure due to inline scripts being disabled', () => {
+ const logger = mockLogger();
+ const claim = jest.fn(() => {
+ throw Object.assign(new Error(), {
+ response:
+ '{"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"cannot execute [inline] scripts"}],"type":"search_phase_execution_exception","reason":"all shards failed","phase":"query","grouped":true,"failed_shards":[{"shard":0,"index":".kibana_task_manager_1","node":"24A4QbjHSK6prvtopAKLKw","reason":{"type":"illegal_argument_exception","reason":"cannot execute [inline] scripts"}}],"caused_by":{"type":"illegal_argument_exception","reason":"cannot execute [inline] scripts","caused_by":{"type":"illegal_argument_exception","reason":"cannot execute [inline] scripts"}}},"status":400}',
+ });
+ });
+
+ claimAvailableTasks([], claim, 10, logger);
+
+ expect(logger.warn).toHaveBeenCalledTimes(1);
+ expect(logger.warn).toHaveBeenCalledWith(
+ `Task Manager cannot operate when inline scripts are disabled in Elasticsearch`
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.ts
new file mode 100644
index 00000000000000..8a506cca699dec
--- /dev/null
+++ b/x-pack/plugins/task_manager/server/polling_lifecycle.ts
@@ -0,0 +1,259 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Subject, Observable, Subscription } from 'rxjs';
+
+import { performance } from 'perf_hooks';
+
+import { pipe } from 'fp-ts/lib/pipeable';
+import { Option, some, map as mapOptional } from 'fp-ts/lib/Option';
+import { Logger } from '../../../../src/core/server';
+
+import { Result, asErr, mapErr } from './lib/result_type';
+import { ManagedConfiguration } from './lib/create_managed_configuration';
+import { TaskManagerConfig } from './config';
+
+import {
+ TaskMarkRunning,
+ TaskRun,
+ TaskClaim,
+ TaskRunRequest,
+ asTaskRunRequestEvent,
+} from './task_events';
+import { fillPool, FillPoolResult } from './lib/fill_pool';
+import { Middleware } from './lib/middleware';
+import { intervalFromNow } from './lib/intervals';
+import { ConcreteTaskInstance } from './task';
+import {
+ createTaskPoller,
+ PollingError,
+ PollingErrorType,
+ createObservableMonitor,
+} from './polling';
+import { TaskPool } from './task_pool';
+import { TaskManagerRunner, TaskRunner } from './task_runner';
+import { TaskStore, OwnershipClaimingOpts, ClaimOwnershipResult } from './task_store';
+import { identifyEsError } from './lib/identify_es_error';
+import { BufferedTaskStore } from './buffered_task_store';
+import { TaskTypeDictionary } from './task_type_dictionary';
+
+export type TaskPollingLifecycleOpts = {
+ logger: Logger;
+ definitions: TaskTypeDictionary;
+ taskStore: TaskStore;
+ config: TaskManagerConfig;
+ middleware: Middleware;
+} & ManagedConfiguration;
+
+export type TaskLifecycleEvent = TaskMarkRunning | TaskRun | TaskClaim | TaskRunRequest;
+
+/**
+ * The public interface into the task manager system.
+ */
+export class TaskPollingLifecycle {
+ private definitions: TaskTypeDictionary;
+
+ private store: TaskStore;
+ private bufferedStore: BufferedTaskStore;
+
+ private logger: Logger;
+ private pool: TaskPool;
+ // all task related events (task claimed, task marked as running, etc.) are emitted through events$
+ private events$ = new Subject();
+ // all on-demand requests we wish to pipe into the poller
+ private claimRequests$ = new Subject>();
+ // the task poller that polls for work on fixed intervals and on demand
+ private poller$: Observable>>;
+ // our subscription to the poller
+ private pollingSubscription: Subscription = Subscription.EMPTY;
+
+ private middleware: Middleware;
+
+ /**
+ * Initializes the task manager, preventing any further addition of middleware,
+ * enabling the task manipulation methods, and beginning the background polling
+ * mechanism.
+ */
+ constructor(opts: TaskPollingLifecycleOpts) {
+ const { logger, middleware, maxWorkersConfiguration$, pollIntervalConfiguration$ } = opts;
+ this.logger = logger;
+ this.middleware = middleware;
+
+ this.definitions = opts.definitions;
+ this.store = opts.taskStore;
+ // pipe store events into the lifecycle event stream
+ this.store.events.subscribe((event) => this.events$.next(event));
+
+ this.bufferedStore = new BufferedTaskStore(this.store, {
+ bufferMaxOperations: opts.config.max_workers,
+ logger: this.logger,
+ });
+
+ this.pool = new TaskPool({
+ logger: this.logger,
+ maxWorkers$: maxWorkersConfiguration$,
+ });
+
+ const {
+ max_poll_inactivity_cycles: maxPollInactivityCycles,
+ poll_interval: pollInterval,
+ } = opts.config;
+ this.poller$ = createObservableMonitor>, Error>(
+ () =>
+ createTaskPoller({
+ logger: this.logger,
+ pollInterval$: pollIntervalConfiguration$,
+ bufferCapacity: opts.config.request_capacity,
+ getCapacity: () => this.pool.availableWorkers,
+ pollRequests$: this.claimRequests$,
+ work: this.pollForWork,
+ // Time out the `work` phase if it takes longer than a certain number of polling cycles
+ // The `work` phase includes the prework needed *before* executing a task
+ // (such as polling for new work, marking tasks as running etc.) but does not
+ // include the time of actually running the task
+ workTimeout: pollInterval * maxPollInactivityCycles,
+ }),
+ {
+ heartbeatInterval: pollInterval,
+ // Time out the poller itself if it has failed to complete the entire stream for a certain amount of time.
+ // This is different that the `work` timeout above, as the poller could enter an invalid state where
+ // it fails to complete a cycle even thought `work` is completing quickly.
+ // We grant it a single cycle longer than the time alotted to `work` so that timing out the `work`
+ // doesn't get short circuited by the monitor reinstantiating the poller all together (a far more expensive
+ // operation than just timing out the `work` internally)
+ inactivityTimeout: pollInterval * (maxPollInactivityCycles + 1),
+ onError: (error) => {
+ this.logger.error(`[Task Poller Monitor]: ${error.message}`);
+ },
+ }
+ );
+ }
+
+ public get events(): Observable {
+ return this.events$;
+ }
+
+ private emitEvent = (event: TaskLifecycleEvent) => {
+ this.events$.next(event);
+ };
+
+ public attemptToRun(task: string) {
+ this.claimRequests$.next(some(task));
+ }
+
+ private createTaskRunnerForTask = (instance: ConcreteTaskInstance) => {
+ return new TaskManagerRunner({
+ logger: this.logger,
+ instance,
+ store: this.bufferedStore,
+ definitions: this.definitions,
+ beforeRun: this.middleware.beforeRun,
+ beforeMarkRunning: this.middleware.beforeMarkRunning,
+ onTaskEvent: this.emitEvent,
+ });
+ };
+
+ public get isStarted() {
+ return !this.pollingSubscription.closed;
+ }
+
+ private pollForWork = async (...tasksToClaim: string[]): Promise => {
+ return fillPool(
+ // claim available tasks
+ () =>
+ claimAvailableTasks(
+ tasksToClaim.splice(0, this.pool.availableWorkers),
+ this.store.claimAvailableTasks,
+ this.pool.availableWorkers,
+ this.logger
+ ),
+ // wrap each task in a Task Runner
+ this.createTaskRunnerForTask,
+ // place tasks in the Task Pool
+ async (tasks: TaskRunner[]) => await this.pool.run(tasks)
+ );
+ };
+
+ /**
+ * Starts up the task manager and starts picking up tasks.
+ */
+ public start() {
+ if (!this.isStarted) {
+ this.pollingSubscription = this.poller$.subscribe(
+ mapErr((error: PollingError) => {
+ if (error.type === PollingErrorType.RequestCapacityReached) {
+ pipe(
+ error.data,
+ mapOptional((id) => this.emitEvent(asTaskRunRequestEvent(id, asErr(error))))
+ );
+ }
+ this.logger.error(error.message);
+ })
+ );
+ }
+ }
+
+ /**
+ * Stops the task manager and cancels running tasks.
+ */
+ public stop() {
+ if (this.isStarted) {
+ this.pollingSubscription.unsubscribe();
+ this.pool.cancelRunningTasks();
+ }
+ }
+}
+
+export async function claimAvailableTasks(
+ claimTasksById: string[],
+ claim: (opts: OwnershipClaimingOpts) => Promise,
+ availableWorkers: number,
+ logger: Logger
+) {
+ if (availableWorkers > 0) {
+ performance.mark('claimAvailableTasks_start');
+
+ try {
+ const { docs, claimedTasks } = await claim({
+ size: availableWorkers,
+ claimOwnershipUntil: intervalFromNow('30s')!,
+ claimTasksById,
+ });
+
+ if (claimedTasks === 0) {
+ performance.mark('claimAvailableTasks.noTasks');
+ }
+ performance.mark('claimAvailableTasks_stop');
+ performance.measure(
+ 'claimAvailableTasks',
+ 'claimAvailableTasks_start',
+ 'claimAvailableTasks_stop'
+ );
+
+ if (docs.length !== claimedTasks) {
+ logger.warn(
+ `[Task Ownership error]: ${claimedTasks} tasks were claimed by Kibana, but ${
+ docs.length
+ } task(s) were fetched (${docs.map((doc) => doc.id).join(', ')})`
+ );
+ }
+ return docs;
+ } catch (ex) {
+ if (identifyEsError(ex).includes('cannot execute [inline] scripts')) {
+ logger.warn(
+ `Task Manager cannot operate when inline scripts are disabled in Elasticsearch`
+ );
+ } else {
+ throw ex;
+ }
+ }
+ } else {
+ performance.mark('claimAvailableTasks.noAvailableWorkers');
+ logger.debug(
+ `[Task Ownership]: Task Manager has skipped Claiming Ownership of available tasks at it has ran out Available Workers.`
+ );
+ }
+ return [];
+}
diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts
index ac98fbbda5aa2e..7cdbd8b11bb064 100644
--- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts
+++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts
@@ -23,23 +23,23 @@ import {
SortByRunAtAndRetryAt,
} from './mark_available_tasks_as_claimed';
-import { TaskDictionary, TaskDefinition } from '../task';
+import { TaskTypeDictionary } from '../task_type_dictionary';
+import { mockLogger } from '../test_utils';
describe('mark_available_tasks_as_claimed', () => {
test('generates query matching tasks to be claimed when polling for tasks', () => {
- const definitions: TaskDictionary = {
+ const definitions = new TaskTypeDictionary(mockLogger());
+ definitions.registerTaskDefinitions({
sampleTask: {
- type: 'sampleTask',
title: 'title',
maxAttempts: 5,
createTaskRunner: () => ({ run: () => Promise.resolve() }),
},
otherTask: {
- type: 'otherTask',
title: 'title',
createTaskRunner: () => ({ run: () => Promise.resolve() }),
},
- };
+ });
const defaultMaxAttempts = 1;
const taskManagerId = '3478fg6-82374f6-83467gf5-384g6f';
const claimOwnershipUntil = '2019-02-12T21:01:22.479Z';
@@ -53,7 +53,7 @@ describe('mark_available_tasks_as_claimed', () => {
// Either task has an schedule or the attempts < the maximum configured
shouldBeOneOf(
TaskWithSchedule,
- ...Object.entries(definitions).map(([type, { maxAttempts }]) =>
+ ...Array.from(definitions).map(([type, { maxAttempts }]) =>
taskWithLessThanMaxAttempts(type, maxAttempts || defaultMaxAttempts)
)
)
diff --git a/x-pack/plugins/task_manager/server/task.ts b/x-pack/plugins/task_manager/server/task.ts
index 4cb0802887417e..6551bd47ef9e73 100644
--- a/x-pack/plugins/task_manager/server/task.ts
+++ b/x-pack/plugins/task_manager/server/task.ts
@@ -24,12 +24,6 @@ import Joi from 'joi';
*/
type Require = Omit & Required>;
-/**
- * A loosely typed definition of the elasticjs wrapper. It's beyond the scope
- * of this work to try to make a comprehensive type definition of this.
- */
-export type ElasticJs = (action: string, args: unknown) => Promise;
-
/**
* The run context is passed into a task's run function as its sole argument.
*/
@@ -154,13 +148,6 @@ export const validateTaskDefinition = Joi.object({
getRetry: Joi.func().optional(),
}).default();
-/**
- * A dictionary mapping task types to their definitions.
- */
-export interface TaskDictionary {
- [taskType: string]: T;
-}
-
export enum TaskStatus {
Idle = 'idle',
Claiming = 'claiming',
diff --git a/x-pack/plugins/task_manager/server/task_manager.mock.ts b/x-pack/plugins/task_manager/server/task_manager.mock.ts
deleted file mode 100644
index 1fc626e7d58d64..00000000000000
--- a/x-pack/plugins/task_manager/server/task_manager.mock.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { TaskManagerSetupContract, TaskManagerStartContract } from './plugin';
-
-export const taskManagerMock = {
- setup(overrides: Partial> = {}) {
- const mocked: jest.Mocked = {
- registerTaskDefinitions: jest.fn(),
- addMiddleware: jest.fn(),
- ...overrides,
- };
- return mocked;
- },
- start(overrides: Partial> = {}) {
- const mocked: jest.Mocked = {
- ensureScheduled: jest.fn(),
- schedule: jest.fn(),
- fetch: jest.fn(),
- get: jest.fn(),
- runNow: jest.fn(),
- remove: jest.fn(),
- ...overrides,
- };
- return mocked;
- },
-};
diff --git a/x-pack/plugins/task_manager/server/task_manager.test.ts b/x-pack/plugins/task_manager/server/task_manager.test.ts
deleted file mode 100644
index cf7f9e2a7cff38..00000000000000
--- a/x-pack/plugins/task_manager/server/task_manager.test.ts
+++ /dev/null
@@ -1,499 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import _ from 'lodash';
-import sinon from 'sinon';
-import { Subject } from 'rxjs';
-import { none } from 'fp-ts/lib/Option';
-
-import {
- asTaskMarkRunningEvent,
- asTaskRunEvent,
- asTaskClaimEvent,
- asTaskRunRequestEvent,
-} from './task_events';
-import {
- TaskManager,
- claimAvailableTasks,
- awaitTaskRunResult,
- TaskLifecycleEvent,
-} from './task_manager';
-import { savedObjectsRepositoryMock } from '../../../../src/core/server/mocks';
-import { SavedObjectsSerializer, SavedObjectTypeRegistry } from '../../../../src/core/server';
-import { mockLogger } from './test_utils';
-import { asErr, asOk } from './lib/result_type';
-import { ConcreteTaskInstance, TaskLifecycleResult, TaskStatus } from './task';
-import { Middleware } from './lib/middleware';
-
-const savedObjectsClient = savedObjectsRepositoryMock.create();
-const serializer = new SavedObjectsSerializer(new SavedObjectTypeRegistry());
-
-describe('TaskManager', () => {
- let clock: sinon.SinonFakeTimers;
-
- const config = {
- enabled: true,
- max_workers: 10,
- index: 'foo',
- max_attempts: 9,
- poll_interval: 6000000,
- max_poll_inactivity_cycles: 10,
- request_capacity: 1000,
- };
- const taskManagerOpts = {
- config,
- savedObjectsRepository: savedObjectsClient,
- serializer,
- callAsInternalUser: jest.fn(),
- logger: mockLogger(),
- taskManagerId: 'some-uuid',
- };
-
- beforeEach(() => {
- clock = sinon.useFakeTimers();
- });
-
- afterEach(() => clock.restore());
-
- test('throws if no valid UUID is available', async () => {
- expect(() => {
- new TaskManager({
- ...taskManagerOpts,
- taskManagerId: '',
- });
- }).toThrowErrorMatchingInlineSnapshot(
- `"TaskManager is unable to start as Kibana has no valid UUID assigned to it."`
- );
- });
-
- test('allows and queues scheduling tasks before starting', async () => {
- const client = new TaskManager(taskManagerOpts);
- client.registerTaskDefinitions({
- foo: {
- type: 'foo',
- title: 'Foo',
- createTaskRunner: jest.fn(),
- },
- });
- const task = {
- taskType: 'foo',
- params: {},
- state: {},
- };
- savedObjectsClient.create.mockResolvedValueOnce({
- id: '1',
- type: 'task',
- attributes: {},
- references: [],
- });
- const promise = client.schedule(task);
- client.start();
- await promise;
-
- expect(savedObjectsClient.create).toHaveBeenCalled();
- });
-
- test('allows scheduling tasks after starting', async () => {
- const client = new TaskManager(taskManagerOpts);
- client.registerTaskDefinitions({
- foo: {
- type: 'foo',
- title: 'Foo',
- createTaskRunner: jest.fn(),
- },
- });
- client.start();
- const task = {
- taskType: 'foo',
- params: {},
- state: {},
- };
- savedObjectsClient.create.mockResolvedValueOnce({
- id: '1',
- type: 'task',
- attributes: {},
- references: [],
- });
- await client.schedule(task);
- expect(savedObjectsClient.create).toHaveBeenCalled();
- });
-
- test('allows scheduling existing tasks that may have already been scheduled', async () => {
- const client = new TaskManager(taskManagerOpts);
- client.registerTaskDefinitions({
- foo: {
- type: 'foo',
- title: 'Foo',
- createTaskRunner: jest.fn(),
- },
- });
- savedObjectsClient.create.mockRejectedValueOnce({
- statusCode: 409,
- });
-
- client.start();
-
- const result = await client.ensureScheduled({
- id: 'my-foo-id',
- taskType: 'foo',
- params: {},
- state: {},
- });
-
- expect(result.id).toEqual('my-foo-id');
- });
-
- test('doesnt ignore failure to scheduling existing tasks for reasons other than already being scheduled', async () => {
- const client = new TaskManager(taskManagerOpts);
- client.registerTaskDefinitions({
- foo: {
- type: 'foo',
- title: 'Foo',
- createTaskRunner: jest.fn(),
- },
- });
- savedObjectsClient.create.mockRejectedValueOnce({
- statusCode: 500,
- });
-
- client.start();
-
- return expect(
- client.ensureScheduled({
- id: 'my-foo-id',
- taskType: 'foo',
- params: {},
- state: {},
- })
- ).rejects.toMatchObject({
- statusCode: 500,
- });
- });
-
- test('doesnt allow naively rescheduling existing tasks that have already been scheduled', async () => {
- const client = new TaskManager(taskManagerOpts);
- client.registerTaskDefinitions({
- foo: {
- type: 'foo',
- title: 'Foo',
- createTaskRunner: jest.fn(),
- },
- });
- savedObjectsClient.create.mockRejectedValueOnce({
- statusCode: 409,
- });
-
- client.start();
-
- return expect(
- client.schedule({
- id: 'my-foo-id',
- taskType: 'foo',
- params: {},
- state: {},
- })
- ).rejects.toMatchObject({
- statusCode: 409,
- });
- });
-
- test('allows and queues removing tasks before starting', async () => {
- const client = new TaskManager(taskManagerOpts);
- savedObjectsClient.delete.mockResolvedValueOnce({});
- const promise = client.remove('1');
- client.start();
- await promise;
- expect(savedObjectsClient.delete).toHaveBeenCalled();
- });
-
- test('allows removing tasks after starting', async () => {
- const client = new TaskManager(taskManagerOpts);
- client.start();
- savedObjectsClient.delete.mockResolvedValueOnce({});
- await client.remove('1');
- expect(savedObjectsClient.delete).toHaveBeenCalled();
- });
-
- test('allows and queues fetching tasks before starting', async () => {
- const client = new TaskManager(taskManagerOpts);
- taskManagerOpts.callAsInternalUser.mockResolvedValue({
- hits: {
- total: {
- value: 0,
- },
- hits: [],
- },
- });
- const promise = client.fetch({});
- client.start();
- await promise;
- expect(taskManagerOpts.callAsInternalUser).toHaveBeenCalled();
- });
-
- test('allows fetching tasks after starting', async () => {
- const client = new TaskManager(taskManagerOpts);
- client.start();
- taskManagerOpts.callAsInternalUser.mockResolvedValue({
- hits: {
- total: {
- value: 0,
- },
- hits: [],
- },
- });
- await client.fetch({});
- expect(taskManagerOpts.callAsInternalUser).toHaveBeenCalled();
- });
-
- test('allows middleware registration before starting', () => {
- const client = new TaskManager(taskManagerOpts);
- const middleware: Middleware = {
- beforeSave: jest.fn(async (saveOpts) => saveOpts),
- beforeRun: jest.fn(async (runOpts) => runOpts),
- beforeMarkRunning: jest.fn(async (runOpts) => runOpts),
- };
- expect(() => client.addMiddleware(middleware)).not.toThrow();
- });
-
- test('disallows middleware registration after starting', async () => {
- const client = new TaskManager(taskManagerOpts);
- const middleware: Middleware = {
- beforeSave: jest.fn(async (saveOpts) => saveOpts),
- beforeRun: jest.fn(async (runOpts) => runOpts),
- beforeMarkRunning: jest.fn(async (runOpts) => runOpts),
- };
-
- client.start();
- expect(() => client.addMiddleware(middleware)).toThrow(
- /Cannot add middleware after the task manager is initialized/i
- );
- });
-
- describe('runNow', () => {
- describe('awaitTaskRunResult', () => {
- test('resolves when the task run succeeds', () => {
- const events$ = new Subject();
- const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
- const getLifecycle = jest.fn();
-
- const result = awaitTaskRunResult(id, events$, getLifecycle);
-
- const task = { id } as ConcreteTaskInstance;
- events$.next(asTaskRunEvent(id, asOk(task)));
-
- return expect(result).resolves.toEqual({ id });
- });
-
- test('rejects when the task run fails', () => {
- const events$ = new Subject();
- const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
- const getLifecycle = jest.fn();
-
- const result = awaitTaskRunResult(id, events$, getLifecycle);
-
- const task = { id } as ConcreteTaskInstance;
- events$.next(asTaskClaimEvent(id, asOk(task)));
- events$.next(asTaskMarkRunningEvent(id, asOk(task)));
- events$.next(asTaskRunEvent(id, asErr(new Error('some thing gone wrong'))));
-
- return expect(result).rejects.toMatchInlineSnapshot(
- `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2": Error: some thing gone wrong]`
- );
- });
-
- test('rejects when the task mark as running fails', () => {
- const events$ = new Subject();
- const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
- const getLifecycle = jest.fn();
-
- const result = awaitTaskRunResult(id, events$, getLifecycle);
-
- const task = { id } as ConcreteTaskInstance;
- events$.next(asTaskClaimEvent(id, asOk(task)));
- events$.next(asTaskMarkRunningEvent(id, asErr(new Error('some thing gone wrong'))));
-
- return expect(result).rejects.toMatchInlineSnapshot(
- `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2": Error: some thing gone wrong]`
- );
- });
-
- test('when a task claim fails we ensure the task exists', async () => {
- const events$ = new Subject();
- const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
- const getLifecycle = jest.fn(async () => TaskLifecycleResult.NotFound);
-
- const result = awaitTaskRunResult(id, events$, getLifecycle);
-
- events$.next(asTaskClaimEvent(id, asErr(none)));
-
- await expect(result).rejects.toEqual(
- new Error(`Failed to run task "${id}" as it does not exist`)
- );
-
- expect(getLifecycle).toHaveBeenCalledWith(id);
- });
-
- test('when a task claim fails we ensure the task isnt already claimed', async () => {
- const events$ = new Subject();
- const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
- const getLifecycle = jest.fn(async () => TaskStatus.Claiming);
-
- const result = awaitTaskRunResult(id, events$, getLifecycle);
-
- events$.next(asTaskClaimEvent(id, asErr(none)));
-
- await expect(result).rejects.toEqual(
- new Error(`Failed to run task "${id}" as it is currently running`)
- );
-
- expect(getLifecycle).toHaveBeenCalledWith(id);
- });
-
- test('when a task claim fails we ensure the task isnt already running', async () => {
- const events$ = new Subject();
- const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
- const getLifecycle = jest.fn(async () => TaskStatus.Running);
-
- const result = awaitTaskRunResult(id, events$, getLifecycle);
-
- events$.next(asTaskClaimEvent(id, asErr(none)));
-
- await expect(result).rejects.toEqual(
- new Error(`Failed to run task "${id}" as it is currently running`)
- );
-
- expect(getLifecycle).toHaveBeenCalledWith(id);
- });
-
- test('rejects when the task run fails due to capacity', async () => {
- const events$ = new Subject();
- const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
- const getLifecycle = jest.fn(async () => TaskStatus.Idle);
-
- const result = awaitTaskRunResult(id, events$, getLifecycle);
-
- events$.next(asTaskRunRequestEvent(id, asErr(new Error('failed to buffer request'))));
-
- await expect(result).rejects.toEqual(
- new Error(
- `Failed to run task "${id}" as Task Manager is at capacity, please try again later`
- )
- );
- expect(getLifecycle).not.toHaveBeenCalled();
- });
-
- test('when a task claim fails we return the underlying error if the task is idle', async () => {
- const events$ = new Subject();
- const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
- const getLifecycle = jest.fn(async () => TaskStatus.Idle);
-
- const result = awaitTaskRunResult(id, events$, getLifecycle);
-
- events$.next(asTaskClaimEvent(id, asErr(none)));
-
- await expect(result).rejects.toMatchInlineSnapshot(
- `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2" for unknown reason (Current Task Lifecycle is "idle")]`
- );
-
- expect(getLifecycle).toHaveBeenCalledWith(id);
- });
-
- test('when a task claim fails we return the underlying error if the task is failed', async () => {
- const events$ = new Subject();
- const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
- const getLifecycle = jest.fn(async () => TaskStatus.Failed);
-
- const result = awaitTaskRunResult(id, events$, getLifecycle);
-
- events$.next(asTaskClaimEvent(id, asErr(none)));
-
- await expect(result).rejects.toMatchInlineSnapshot(
- `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2" for unknown reason (Current Task Lifecycle is "failed")]`
- );
-
- expect(getLifecycle).toHaveBeenCalledWith(id);
- });
-
- test('ignores task run success of other tasks', () => {
- const events$ = new Subject();
- const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
- const differentTask = '4bebf429-181b-4518-bb7d-b4246d8a35f0';
- const getLifecycle = jest.fn();
-
- const result = awaitTaskRunResult(id, events$, getLifecycle);
-
- const task = { id } as ConcreteTaskInstance;
- const otherTask = { id: differentTask } as ConcreteTaskInstance;
- events$.next(asTaskClaimEvent(id, asOk(task)));
- events$.next(asTaskClaimEvent(differentTask, asOk(otherTask)));
-
- events$.next(asTaskRunEvent(differentTask, asOk(task)));
-
- events$.next(asTaskRunEvent(id, asErr(new Error('some thing gone wrong'))));
-
- return expect(result).rejects.toMatchInlineSnapshot(
- `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2": Error: some thing gone wrong]`
- );
- });
- });
- });
-
- describe('claimAvailableTasks', () => {
- test('should claim Available Tasks when there are available workers', () => {
- const logger = mockLogger();
- const claim = jest.fn(() => Promise.resolve({ docs: [], claimedTasks: 0 }));
-
- const availableWorkers = 1;
-
- claimAvailableTasks([], claim, availableWorkers, logger);
-
- expect(claim).toHaveBeenCalledTimes(1);
- });
-
- test('should not claim Available Tasks when there are no available workers', () => {
- const logger = mockLogger();
- const claim = jest.fn(() => Promise.resolve({ docs: [], claimedTasks: 0 }));
-
- const availableWorkers = 0;
-
- claimAvailableTasks([], claim, availableWorkers, logger);
-
- expect(claim).not.toHaveBeenCalled();
- });
-
- /**
- * This handles the case in which Elasticsearch has had inline script disabled.
- * This is achieved by setting the `script.allowed_types` flag on Elasticsearch to `none`
- */
- test('handles failure due to inline scripts being disabled', () => {
- const logger = mockLogger();
- const claim = jest.fn(() => {
- throw Object.assign(new Error(), {
- msg: '[illegal_argument_exception] cannot execute [inline] scripts',
- path: '/.kibana_task_manager/_update_by_query',
- query: {
- ignore_unavailable: true,
- refresh: true,
- max_docs: 200,
- conflicts: 'proceed',
- },
- body:
- '{"query":{"bool":{"must":[{"term":{"type":"task"}},{"bool":{"must":[{"bool":{"should":[{"bool":{"must":[{"term":{"task.status":"idle"}},{"range":{"task.runAt":{"lte":"now"}}}]}},{"bool":{"must":[{"bool":{"should":[{"term":{"task.status":"running"}},{"term":{"task.status":"claiming"}}]}},{"range":{"task.retryAt":{"lte":"now"}}}]}}]}},{"bool":{"should":[{"exists":{"field":"task.schedule"}},{"bool":{"must":[{"term":{"task.taskType":"vis_telemetry"}},{"range":{"task.attempts":{"lt":3}}}]}},{"bool":{"must":[{"term":{"task.taskType":"lens_telemetry"}},{"range":{"task.attempts":{"lt":3}}}]}},{"bool":{"must":[{"term":{"task.taskType":"actions:.server-log"}},{"range":{"task.attempts":{"lt":1}}}]}},{"bool":{"must":[{"term":{"task.taskType":"actions:.slack"}},{"range":{"task.attempts":{"lt":1}}}]}},{"bool":{"must":[{"term":{"task.taskType":"actions:.email"}},{"range":{"task.attempts":{"lt":1}}}]}},{"bool":{"must":[{"term":{"task.taskType":"actions:.index"}},{"range":{"task.attempts":{"lt":1}}}]}},{"bool":{"must":[{"term":{"task.taskType":"actions:.pagerduty"}},{"range":{"task.attempts":{"lt":1}}}]}},{"bool":{"must":[{"term":{"task.taskType":"actions:.webhook"}},{"range":{"task.attempts":{"lt":1}}}]}}]}}]}}]}},"sort":{"_script":{"type":"number","order":"asc","script":{"lang":"expression","source":"doc[\'task.retryAt\'].value || doc[\'task.runAt\'].value"}}},"seq_no_primary_term":true,"script":{"source":"ctx._source.task.ownerId=params.ownerId; ctx._source.task.status=params.status; ctx._source.task.retryAt=params.retryAt;","lang":"painless","params":{"ownerId":"kibana:5b2de169-2785-441b-ae8c-186a1936b17d","retryAt":"2019-10-31T13:35:43.579Z","status":"claiming"}}}',
- statusCode: 400,
- response:
- '{"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"cannot execute [inline] scripts"}],"type":"search_phase_execution_exception","reason":"all shards failed","phase":"query","grouped":true,"failed_shards":[{"shard":0,"index":".kibana_task_manager_1","node":"24A4QbjHSK6prvtopAKLKw","reason":{"type":"illegal_argument_exception","reason":"cannot execute [inline] scripts"}}],"caused_by":{"type":"illegal_argument_exception","reason":"cannot execute [inline] scripts","caused_by":{"type":"illegal_argument_exception","reason":"cannot execute [inline] scripts"}}},"status":400}',
- });
- });
-
- claimAvailableTasks([], claim, 10, logger);
-
- expect(logger.warn).toHaveBeenCalledTimes(1);
- expect(logger.warn.mock.calls[0][0]).toMatchInlineSnapshot(
- `"Task Manager cannot operate when inline scripts are disabled in Elasticsearch"`
- );
- });
- });
-});
diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts
deleted file mode 100644
index cc611e124ea7b4..00000000000000
--- a/x-pack/plugins/task_manager/server/task_manager.ts
+++ /dev/null
@@ -1,544 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { Subject, Observable, Subscription } from 'rxjs';
-import { filter } from 'rxjs/operators';
-
-import { performance } from 'perf_hooks';
-
-import { pipe } from 'fp-ts/lib/pipeable';
-import { Option, some, map as mapOptional, getOrElse } from 'fp-ts/lib/Option';
-
-import {
- SavedObjectsSerializer,
- ILegacyScopedClusterClient,
- ISavedObjectsRepository,
-} from '../../../../src/core/server';
-import { Result, asOk, asErr, either, map, mapErr, promiseResult } from './lib/result_type';
-import { createManagedConfiguration } from './lib/create_managed_configuration';
-import { TaskManagerConfig } from './config';
-
-import { Logger } from './types';
-import {
- TaskMarkRunning,
- TaskRun,
- TaskClaim,
- TaskRunRequest,
- isTaskRunEvent,
- isTaskClaimEvent,
- isTaskRunRequestEvent,
- asTaskRunRequestEvent,
-} from './task_events';
-import { fillPool, FillPoolResult } from './lib/fill_pool';
-import { addMiddlewareToChain, BeforeSaveMiddlewareParams, Middleware } from './lib/middleware';
-import { sanitizeTaskDefinitions } from './lib/sanitize_task_definitions';
-import { intervalFromNow } from './lib/intervals';
-import {
- TaskDefinition,
- TaskDictionary,
- ConcreteTaskInstance,
- RunContext,
- TaskInstanceWithId,
- TaskInstanceWithDeprecatedFields,
- TaskLifecycle,
- TaskLifecycleResult,
- TaskStatus,
- ElasticJs,
-} from './task';
-import {
- createTaskPoller,
- PollingError,
- PollingErrorType,
- createObservableMonitor,
-} from './polling';
-import { TaskPool } from './task_pool';
-import { TaskManagerRunner, TaskRunner } from './task_runner';
-import {
- FetchResult,
- TaskStore,
- OwnershipClaimingOpts,
- ClaimOwnershipResult,
- SearchOpts,
-} from './task_store';
-import { identifyEsError } from './lib/identify_es_error';
-import { ensureDeprecatedFieldsAreCorrected } from './lib/correct_deprecated_fields';
-import { BufferedTaskStore } from './buffered_task_store';
-
-const VERSION_CONFLICT_STATUS = 409;
-
-export interface TaskManagerOpts {
- logger: Logger;
- config: TaskManagerConfig;
- callAsInternalUser: ILegacyScopedClusterClient['callAsInternalUser'];
- savedObjectsRepository: ISavedObjectsRepository;
- serializer: SavedObjectsSerializer;
- taskManagerId: string;
-}
-
-interface RunNowResult {
- id: string;
-}
-
-export type TaskLifecycleEvent = TaskMarkRunning | TaskRun | TaskClaim | TaskRunRequest;
-
-/*
- * The TaskManager is the public interface into the task manager system. This glues together
- * all of the disparate modules in one integration point. The task manager operates in two different ways:
- *
- * - pre-init, it allows middleware registration, but disallows task manipulation
- * - post-init, it disallows middleware registration, but allows task manipulation
- *
- * Due to its complexity, this is mostly tested by integration tests (see readme).
- */
-
-/**
- * The public interface into the task manager system.
- */
-export class TaskManager {
- private definitions: TaskDictionary = {};
-
- private store: TaskStore;
- private bufferedStore: BufferedTaskStore;
-
- private logger: Logger;
- private pool: TaskPool;
- // all task related events (task claimed, task marked as running, etc.) are emitted through events$
- private events$ = new Subject();
- // all on-demand requests we wish to pipe into the poller
- private claimRequests$ = new Subject>();
- // the task poller that polls for work on fixed intervals and on demand
- private poller$: Observable>>;
- // our subscription to the poller
- private pollingSubscription: Subscription = Subscription.EMPTY;
-
- private startQueue: Array<() => void> = [];
- private middleware = {
- beforeSave: async (saveOpts: BeforeSaveMiddlewareParams) => saveOpts,
- beforeRun: async (runOpts: RunContext) => runOpts,
- beforeMarkRunning: async (runOpts: RunContext) => runOpts,
- };
-
- /**
- * Initializes the task manager, preventing any further addition of middleware,
- * enabling the task manipulation methods, and beginning the background polling
- * mechanism.
- */
- constructor(opts: TaskManagerOpts) {
- this.logger = opts.logger;
-
- const { taskManagerId } = opts;
- if (!taskManagerId) {
- this.logger.error(
- `TaskManager is unable to start as there the Kibana UUID is invalid (value of the "server.uuid" configuration is ${taskManagerId})`
- );
- throw new Error(`TaskManager is unable to start as Kibana has no valid UUID assigned to it.`);
- } else {
- this.logger.info(`TaskManager is identified by the Kibana UUID: ${taskManagerId}`);
- }
-
- this.store = new TaskStore({
- serializer: opts.serializer,
- savedObjectsRepository: opts.savedObjectsRepository,
- callCluster: (opts.callAsInternalUser as unknown) as ElasticJs,
- index: opts.config.index,
- maxAttempts: opts.config.max_attempts,
- definitions: this.definitions,
- taskManagerId: `kibana:${taskManagerId}`,
- });
- // pipe store events into the TaskManager's event stream
- this.store.events.subscribe((event) => this.events$.next(event));
-
- const { maxWorkersConfiguration$, pollIntervalConfiguration$ } = createManagedConfiguration({
- logger: this.logger,
- errors$: this.store.errors$,
- startingMaxWorkers: opts.config.max_workers,
- startingPollInterval: opts.config.poll_interval,
- });
-
- this.bufferedStore = new BufferedTaskStore(this.store, {
- bufferMaxOperations: opts.config.max_workers,
- logger: this.logger,
- });
-
- this.pool = new TaskPool({
- logger: this.logger,
- maxWorkers$: maxWorkersConfiguration$,
- });
-
- const {
- max_poll_inactivity_cycles: maxPollInactivityCycles,
- poll_interval: pollInterval,
- } = opts.config;
- this.poller$ = createObservableMonitor>, Error>(
- () =>
- createTaskPoller({
- logger: this.logger,
- pollInterval$: pollIntervalConfiguration$,
- bufferCapacity: opts.config.request_capacity,
- getCapacity: () => this.pool.availableWorkers,
- pollRequests$: this.claimRequests$,
- work: this.pollForWork,
- // Time out the `work` phase if it takes longer than a certain number of polling cycles
- // The `work` phase includes the prework needed *before* executing a task
- // (such as polling for new work, marking tasks as running etc.) but does not
- // include the time of actually running the task
- workTimeout: pollInterval * maxPollInactivityCycles,
- }),
- {
- heartbeatInterval: pollInterval,
- // Time out the poller itself if it has failed to complete the entire stream for a certain amount of time.
- // This is different that the `work` timeout above, as the poller could enter an invalid state where
- // it fails to complete a cycle even thought `work` is completing quickly.
- // We grant it a single cycle longer than the time alotted to `work` so that timing out the `work`
- // doesn't get short circuited by the monitor reinstantiating the poller all together (a far more expensive
- // operation than just timing out the `work` internally)
- inactivityTimeout: pollInterval * (maxPollInactivityCycles + 1),
- onError: (error) => {
- this.logger.error(`[Task Poller Monitor]: ${error.message}`);
- },
- }
- );
- }
-
- private emitEvent = (event: TaskLifecycleEvent) => {
- this.events$.next(event);
- };
-
- private attemptToRun(task: string) {
- this.claimRequests$.next(some(task));
- }
-
- private createTaskRunnerForTask = (instance: ConcreteTaskInstance) => {
- return new TaskManagerRunner({
- logger: this.logger,
- instance,
- store: this.bufferedStore,
- definitions: this.definitions,
- beforeRun: this.middleware.beforeRun,
- beforeMarkRunning: this.middleware.beforeMarkRunning,
- onTaskEvent: this.emitEvent,
- });
- };
-
- public get isStarted() {
- return !this.pollingSubscription.closed;
- }
-
- private pollForWork = async (...tasksToClaim: string[]): Promise => {
- return fillPool(
- // claim available tasks
- () =>
- claimAvailableTasks(
- tasksToClaim.splice(0, this.pool.availableWorkers),
- this.store.claimAvailableTasks,
- this.pool.availableWorkers,
- this.logger
- ),
- // wrap each task in a Task Runner
- this.createTaskRunnerForTask,
- // place tasks in the Task Pool
- async (tasks: TaskRunner[]) => await this.pool.run(tasks)
- );
- };
-
- /**
- * Starts up the task manager and starts picking up tasks.
- */
- public start() {
- if (!this.isStarted) {
- // Some calls are waiting until task manager is started
- this.startQueue.forEach((fn) => fn());
- this.startQueue = [];
-
- this.pollingSubscription = this.poller$.subscribe(
- mapErr((error: PollingError) => {
- if (error.type === PollingErrorType.RequestCapacityReached) {
- pipe(
- error.data,
- mapOptional((id) => this.emitEvent(asTaskRunRequestEvent(id, asErr(error))))
- );
- }
- this.logger.error(error.message);
- })
- );
- }
- }
-
- private async waitUntilStarted() {
- if (!this.isStarted) {
- await new Promise((resolve) => {
- this.startQueue.push(resolve);
- });
- }
- }
-
- /**
- * Stops the task manager and cancels running tasks.
- */
- public stop() {
- if (this.isStarted) {
- this.pollingSubscription.unsubscribe();
- this.pool.cancelRunningTasks();
- }
- }
-
- /**
- * Method for allowing consumers to register task definitions into the system.
- * @param taskDefinitions - The Kibana task definitions dictionary
- */
- public registerTaskDefinitions(taskDefinitions: TaskDictionary) {
- this.assertUninitialized('register task definitions', Object.keys(taskDefinitions).join(', '));
- const duplicate = Object.keys(taskDefinitions).find((k) => !!this.definitions[k]);
- if (duplicate) {
- throw new Error(`Task ${duplicate} is already defined!`);
- }
-
- try {
- const sanitized = sanitizeTaskDefinitions(taskDefinitions);
-
- Object.assign(this.definitions, sanitized);
- } catch (e) {
- this.logger.error('Could not sanitize task definitions');
- }
- }
-
- /**
- * Adds middleware to the task manager, such as adding security layers, loggers, etc.
- *
- * @param {Middleware} middleware - The middlware being added.
- */
- public addMiddleware(middleware: Middleware) {
- this.assertUninitialized('add middleware');
- const prevMiddleWare = this.middleware;
- this.middleware = addMiddlewareToChain(prevMiddleWare, middleware);
- }
-
- /**
- * Schedules a task.
- *
- * @param task - The task being scheduled.
- * @returns {Promise}
- */
- public async schedule(
- taskInstance: TaskInstanceWithDeprecatedFields,
- options?: Record
- ): Promise {
- await this.waitUntilStarted();
- const { taskInstance: modifiedTask } = await this.middleware.beforeSave({
- ...options,
- taskInstance: ensureDeprecatedFieldsAreCorrected(taskInstance, this.logger),
- });
- return await this.store.schedule(modifiedTask);
- }
-
- /**
- * Run task.
- *
- * @param taskId - The task being scheduled.
- * @returns {Promise}
- */
- public async runNow(taskId: string): Promise {
- await this.waitUntilStarted();
- return new Promise(async (resolve, reject) => {
- awaitTaskRunResult(taskId, this.events$, this.store.getLifecycle.bind(this.store))
- .then(resolve)
- .catch(reject);
-
- this.attemptToRun(taskId);
- });
- }
-
- /**
- * Schedules a task with an Id
- *
- * @param task - The task being scheduled.
- * @returns {Promise}
- */
- public async ensureScheduled(
- taskInstance: TaskInstanceWithId,
- options?: Record
- ): Promise {
- try {
- return await this.schedule(taskInstance, options);
- } catch (err) {
- if (err.statusCode === VERSION_CONFLICT_STATUS) {
- return taskInstance;
- }
- throw err;
- }
- }
-
- /**
- * Fetches a list of scheduled tasks.
- *
- * @param opts - The query options used to filter tasks
- * @returns {Promise}
- */
- public async fetch(opts: SearchOpts): Promise {
- await this.waitUntilStarted();
- return this.store.fetch(opts);
- }
-
- /**
- * Get the current state of a specified task.
- *
- * @param {string} id
- * @returns {Promise}
- */
- public async get(id: string): Promise {
- await this.waitUntilStarted();
- return this.store.get(id);
- }
-
- /**
- * Removes the specified task from the index.
- *
- * @param {string} id
- * @returns {Promise}
- */
- public async remove(id: string): Promise {
- await this.waitUntilStarted();
- return this.store.remove(id);
- }
-
- /**
- * Ensures task manager IS NOT already initialized
- *
- * @param {string} message shown if task manager is already initialized
- * @returns void
- */
- private assertUninitialized(message: string, context?: string) {
- if (this.isStarted) {
- throw new Error(
- `${context ? `[${context}] ` : ''}Cannot ${message} after the task manager is initialized`
- );
- }
- }
-}
-
-export async function claimAvailableTasks(
- claimTasksById: string[],
- claim: (opts: OwnershipClaimingOpts) => Promise,
- availableWorkers: number,
- logger: Logger
-) {
- if (availableWorkers > 0) {
- performance.mark('claimAvailableTasks_start');
-
- try {
- const { docs, claimedTasks } = await claim({
- size: availableWorkers,
- claimOwnershipUntil: intervalFromNow('30s')!,
- claimTasksById,
- });
-
- if (claimedTasks === 0) {
- performance.mark('claimAvailableTasks.noTasks');
- }
- performance.mark('claimAvailableTasks_stop');
- performance.measure(
- 'claimAvailableTasks',
- 'claimAvailableTasks_start',
- 'claimAvailableTasks_stop'
- );
-
- if (docs.length !== claimedTasks) {
- logger.warn(
- `[Task Ownership error]: ${claimedTasks} tasks were claimed by Kibana, but ${
- docs.length
- } task(s) were fetched (${docs.map((doc) => doc.id).join(', ')})`
- );
- }
- return docs;
- } catch (ex) {
- if (identifyEsError(ex).includes('cannot execute [inline] scripts')) {
- logger.warn(
- `Task Manager cannot operate when inline scripts are disabled in Elasticsearch`
- );
- } else {
- throw ex;
- }
- }
- } else {
- performance.mark('claimAvailableTasks.noAvailableWorkers');
- logger.debug(
- `[Task Ownership]: Task Manager has skipped Claiming Ownership of available tasks at it has ran out Available Workers.`
- );
- }
- return [];
-}
-
-export async function awaitTaskRunResult(
- taskId: string,
- events$: Subject,
- getLifecycle: (id: string) => Promise
-): Promise {
- return new Promise((resolve, reject) => {
- const subscription = events$
- // listen for all events related to the current task
- .pipe(filter(({ id }: TaskLifecycleEvent) => id === taskId))
- .subscribe((taskEvent: TaskLifecycleEvent) => {
- if (isTaskClaimEvent(taskEvent)) {
- mapErr(async (error: Option) => {
- // reject if any error event takes place for the requested task
- subscription.unsubscribe();
- return reject(
- map(
- await pipe(
- error,
- mapOptional(async (taskReturnedBySweep) => asOk(taskReturnedBySweep.status)),
- getOrElse(() =>
- // if the error happened in the Claim phase - we try to provide better insight
- // into why we failed to claim by getting the task's current lifecycle status
- promiseResult(getLifecycle(taskId))
- )
- ),
- (taskLifecycleStatus: TaskLifecycle) => {
- if (taskLifecycleStatus === TaskLifecycleResult.NotFound) {
- return new Error(`Failed to run task "${taskId}" as it does not exist`);
- } else if (
- taskLifecycleStatus === TaskStatus.Running ||
- taskLifecycleStatus === TaskStatus.Claiming
- ) {
- return new Error(`Failed to run task "${taskId}" as it is currently running`);
- }
- return new Error(
- `Failed to run task "${taskId}" for unknown reason (Current Task Lifecycle is "${taskLifecycleStatus}")`
- );
- },
- (getLifecycleError: Error) =>
- new Error(
- `Failed to run task "${taskId}" and failed to get current Status:${getLifecycleError}`
- )
- )
- );
- }, taskEvent.event);
- } else {
- either>(
- taskEvent.event,
- (taskInstance: ConcreteTaskInstance) => {
- // resolve if the task has run sucessfully
- if (isTaskRunEvent(taskEvent)) {
- subscription.unsubscribe();
- resolve({ id: taskInstance.id });
- }
- },
- async (error: Error | Option) => {
- // reject if any error event takes place for the requested task
- subscription.unsubscribe();
- if (isTaskRunRequestEvent(taskEvent)) {
- return reject(
- new Error(
- `Failed to run task "${taskId}" as Task Manager is at capacity, please try again later`
- )
- );
- }
- return reject(new Error(`Failed to run task "${taskId}": ${error}`));
- }
- );
- }
- });
- });
-}
diff --git a/x-pack/plugins/task_manager/server/task_pool.ts b/x-pack/plugins/task_manager/server/task_pool.ts
index 44f5f5648c2acf..9f7948ecad34a2 100644
--- a/x-pack/plugins/task_manager/server/task_pool.ts
+++ b/x-pack/plugins/task_manager/server/task_pool.ts
@@ -12,7 +12,7 @@ import { Observable } from 'rxjs';
import moment, { Duration } from 'moment';
import { performance } from 'perf_hooks';
import { padStart } from 'lodash';
-import { Logger } from './types';
+import { Logger } from '../../../../src/core/server';
import { TaskRunner } from './task_runner';
import { isTaskSavedObjectNotFoundError } from './lib/is_task_not_found_error';
diff --git a/x-pack/plugins/task_manager/server/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_runner.test.ts
index c3191dbb349e6c..8fb1df444c603a 100644
--- a/x-pack/plugins/task_manager/server/task_runner.test.ts
+++ b/x-pack/plugins/task_manager/server/task_runner.test.ts
@@ -9,11 +9,12 @@ import sinon from 'sinon';
import { minutesFromNow } from './lib/intervals';
import { asOk, asErr } from './lib/result_type';
import { TaskEvent, asTaskRunEvent, asTaskMarkRunningEvent } from './task_events';
-import { ConcreteTaskInstance, TaskStatus, TaskDictionary, TaskDefinition } from './task';
+import { ConcreteTaskInstance, TaskStatus, TaskDefinition, RunResult } from './task';
import { TaskManagerRunner } from './task_runner';
-import { mockLogger } from './test_utils';
import { SavedObjectsErrorHelpers } from '../../../../src/core/server';
import moment from 'moment';
+import { TaskTypeDictionary } from './task_type_dictionary';
+import { mockLogger } from './test_utils';
let fakeTimer: sinon.SinonFakeTimers;
@@ -67,6 +68,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
async run() {
throw new Error('Dangit!');
@@ -96,9 +98,10 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
async run() {
- return;
+ return { state: {} };
},
}),
},
@@ -124,10 +127,11 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
timeout: `1m`,
createTaskRunner: () => ({
async run() {
- return;
+ return { state: {} };
},
}),
},
@@ -150,10 +154,11 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
timeout: `1m`,
createTaskRunner: () => ({
async run() {
- return;
+ return { state: {} };
},
}),
},
@@ -171,9 +176,10 @@ describe('TaskManagerRunner', () => {
const { runner, store } = testOpts({
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
async run() {
- return { runAt };
+ return { runAt, state: {} };
},
}),
},
@@ -194,9 +200,10 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
async run() {
- return { runAt };
+ return { runAt, state: {} };
},
}),
},
@@ -218,6 +225,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
async run() {
return undefined;
@@ -238,6 +246,7 @@ describe('TaskManagerRunner', () => {
const { runner, logger } = testOpts({
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
async run() {
const promise = new Promise((r) => setTimeout(r, 1000));
@@ -265,6 +274,7 @@ describe('TaskManagerRunner', () => {
const { runner, logger } = testOpts({
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
run: async () => undefined,
}),
@@ -291,6 +301,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
timeout: `${timeoutMinutes}m`,
createTaskRunner: () => ({
run: async () => undefined,
@@ -325,6 +336,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
getRetry: getRetryStub,
createTaskRunner: () => ({
async run() {
@@ -356,6 +368,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
getRetry: getRetryStub,
createTaskRunner: () => ({
async run() {
@@ -388,6 +401,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
getRetry: getRetryStub,
createTaskRunner: () => ({
async run() {
@@ -421,6 +435,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
getRetry: getRetryStub,
createTaskRunner: () => ({
async run() {
@@ -456,6 +471,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
timeout: `${timeoutMinutes}m`,
getRetry: getRetryStub,
createTaskRunner: () => ({
@@ -490,6 +506,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
timeout: `${timeoutMinutes}m`,
getRetry: getRetryStub,
createTaskRunner: () => ({
@@ -522,6 +539,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
timeout: `${timeoutMinutes}m`,
getRetry: getRetryStub,
createTaskRunner: () => ({
@@ -557,6 +575,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
timeout: `${timeoutMinutes}m`,
getRetry: getRetryStub,
createTaskRunner: () => ({
@@ -592,6 +611,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
timeout: `${timeoutMinutes}m`,
getRetry: getRetryStub,
createTaskRunner: () => ({
@@ -625,6 +645,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
timeout: `${timeoutMinutes}m`,
getRetry: getRetryStub,
createTaskRunner: () => ({
@@ -655,6 +676,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
maxAttempts: 3,
createTaskRunner: () => ({
run: async () => {
@@ -688,6 +710,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
maxAttempts: 3,
createTaskRunner: () => ({
run: async () => {
@@ -720,8 +743,8 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
timeout: `1m`,
- getRetry: () => {},
createTaskRunner: () => ({
run: async () => undefined,
}),
@@ -748,8 +771,8 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
timeout: `1m`,
- getRetry: () => {},
createTaskRunner: () => ({
run: async () => undefined,
}),
@@ -777,9 +800,10 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
async run() {
- return {};
+ return { state: {} };
},
}),
},
@@ -803,9 +827,10 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
async run() {
- return { runAt };
+ return { runAt, state: {} };
},
}),
},
@@ -828,6 +853,7 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
async run() {
throw error;
@@ -855,9 +881,10 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
async run() {
- return { error };
+ return { error, state: {} };
},
}),
},
@@ -882,10 +909,11 @@ describe('TaskManagerRunner', () => {
},
definitions: {
bar: {
+ title: 'Bar!',
getRetry: () => false,
createTaskRunner: () => ({
async run() {
- return { error };
+ return { error, state: {} };
},
}),
},
@@ -904,7 +932,7 @@ describe('TaskManagerRunner', () => {
interface TestOpts {
instance?: Partial;
- definitions?: unknown;
+ definitions?: Record>;
onTaskEvent?: (event: TaskEvent) => void;
}
@@ -942,19 +970,24 @@ describe('TaskManagerRunner', () => {
store.update.returns(instance);
+ const definitions = new TaskTypeDictionary(logger);
+ definitions.registerTaskDefinitions({
+ testbar: {
+ title: 'Bar!',
+ createTaskRunner,
+ },
+ });
+ if (opts.definitions) {
+ definitions.registerTaskDefinitions(opts.definitions);
+ }
+
const runner = new TaskManagerRunner({
beforeRun: (context) => Promise.resolve(context),
beforeMarkRunning: (context) => Promise.resolve(context),
logger,
store,
instance,
- definitions: Object.assign(opts.definitions || {}, {
- testbar: {
- type: 'bar',
- title: 'Bar!',
- createTaskRunner,
- },
- }) as TaskDictionary,
+ definitions,
onTaskEvent: opts.onTaskEvent,
});
@@ -972,8 +1005,9 @@ describe('TaskManagerRunner', () => {
const { runner, logger } = testOpts({
definitions: {
bar: {
+ title: 'Bar!',
createTaskRunner: () => ({
- run: async () => result,
+ run: async () => result as RunResult,
}),
},
},
diff --git a/x-pack/plugins/task_manager/server/task_runner.ts b/x-pack/plugins/task_manager/server/task_runner.ts
index ebf13fac2f311e..24a487e3660293 100644
--- a/x-pack/plugins/task_manager/server/task_runner.ts
+++ b/x-pack/plugins/task_manager/server/task_runner.ts
@@ -15,11 +15,11 @@ import { performance } from 'perf_hooks';
import Joi from 'joi';
import { identity, defaults, flow } from 'lodash';
+import { Logger } from '../../../../src/core/server';
import { asOk, asErr, mapErr, eitherAsync, unwrap, mapOk, Result } from './lib/result_type';
import { TaskRun, TaskMarkRunning, asTaskRunEvent, asTaskMarkRunningEvent } from './task_events';
import { intervalFromDate, intervalFromNow } from './lib/intervals';
-import { Logger } from './types';
-import { BeforeRunFunction, BeforeMarkRunningFunction } from './lib/middleware';
+import { Middleware } from './lib/middleware';
import {
CancelFunction,
CancellableTask,
@@ -29,10 +29,10 @@ import {
FailedRunResult,
FailedTaskResult,
TaskDefinition,
- TaskDictionary,
validateRunResult,
TaskStatus,
} from './task';
+import { TaskTypeDictionary } from './task_type_dictionary';
const defaultBackoffPerFailure = 5 * 60 * 1000;
const EMPTY_RUN_RESULT: SuccessfulRunResult = {};
@@ -55,15 +55,13 @@ export interface Updatable {
remove(id: string): Promise;
}
-interface Opts {
+type Opts = {
logger: Logger;
- definitions: TaskDictionary;
+ definitions: TaskTypeDictionary;
instance: ConcreteTaskInstance;
store: Updatable;
- beforeRun: BeforeRunFunction;
- beforeMarkRunning: BeforeMarkRunningFunction;
onTaskEvent?: (event: TaskRun | TaskMarkRunning) => void;
-}
+} & Pick;
/**
* Runs a background task, ensures that errors are properly handled,
@@ -76,11 +74,11 @@ interface Opts {
export class TaskManagerRunner implements TaskRunner {
private task?: CancellableTask;
private instance: ConcreteTaskInstance;
- private definitions: TaskDictionary;
+ private definitions: TaskTypeDictionary;
private logger: Logger;
private bufferedTaskStore: Updatable;
- private beforeRun: BeforeRunFunction;
- private beforeMarkRunning: BeforeMarkRunningFunction;
+ private beforeRun: Middleware['beforeRun'];
+ private beforeMarkRunning: Middleware['beforeMarkRunning'];
private onTaskEvent: (event: TaskRun | TaskMarkRunning) => void;
/**
@@ -129,7 +127,7 @@ export class TaskManagerRunner implements TaskRunner {
* Gets the task defintion from the dictionary.
*/
public get definition() {
- return this.definitions[this.taskType];
+ return this.definitions.get(this.taskType);
}
/**
diff --git a/x-pack/plugins/task_manager/server/task_scheduling.mock.ts b/x-pack/plugins/task_manager/server/task_scheduling.mock.ts
new file mode 100644
index 00000000000000..5a6a369ad7a447
--- /dev/null
+++ b/x-pack/plugins/task_manager/server/task_scheduling.mock.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TaskScheduling } from './task_scheduling';
+
+const createTaskSchedulingMock = () => {
+ return ({
+ ensureScheduled: jest.fn(),
+ schedule: jest.fn(),
+ runNow: jest.fn(),
+ } as unknown) as jest.Mocked;
+};
+
+export const taskSchedulingMock = {
+ create: createTaskSchedulingMock,
+};
diff --git a/x-pack/plugins/task_manager/server/task_scheduling.test.ts b/x-pack/plugins/task_manager/server/task_scheduling.test.ts
new file mode 100644
index 00000000000000..1f7f9250d90147
--- /dev/null
+++ b/x-pack/plugins/task_manager/server/task_scheduling.test.ts
@@ -0,0 +1,319 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import _ from 'lodash';
+import { Subject } from 'rxjs';
+import { none } from 'fp-ts/lib/Option';
+
+import {
+ asTaskMarkRunningEvent,
+ asTaskRunEvent,
+ asTaskClaimEvent,
+ asTaskRunRequestEvent,
+} from './task_events';
+import { TaskLifecycleEvent } from './polling_lifecycle';
+import { taskPollingLifecycleMock } from './polling_lifecycle.mock';
+import { TaskScheduling } from './task_scheduling';
+import { asErr, asOk } from './lib/result_type';
+import { ConcreteTaskInstance, TaskLifecycleResult, TaskStatus } from './task';
+import { createInitialMiddleware } from './lib/middleware';
+import { taskStoreMock } from './task_store.mock';
+import { mockLogger } from './test_utils';
+
+describe('TaskScheduling', () => {
+ const mockTaskStore = taskStoreMock.create({});
+ const mockTaskManager = taskPollingLifecycleMock.create({});
+ const taskSchedulingOpts = {
+ taskStore: mockTaskStore,
+ taskPollingLifecycle: mockTaskManager,
+ logger: mockLogger(),
+ middleware: createInitialMiddleware(),
+ };
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ test('allows scheduling tasks', async () => {
+ const taskScheduling = new TaskScheduling(taskSchedulingOpts);
+ const task = {
+ taskType: 'foo',
+ params: {},
+ state: {},
+ };
+ await taskScheduling.schedule(task);
+ expect(mockTaskStore.schedule).toHaveBeenCalled();
+ });
+
+ test('allows scheduling existing tasks that may have already been scheduled', async () => {
+ const taskScheduling = new TaskScheduling(taskSchedulingOpts);
+ mockTaskStore.schedule.mockRejectedValueOnce({
+ statusCode: 409,
+ });
+
+ const result = await taskScheduling.ensureScheduled({
+ id: 'my-foo-id',
+ taskType: 'foo',
+ params: {},
+ state: {},
+ });
+
+ expect(result.id).toEqual('my-foo-id');
+ });
+
+ test('doesnt ignore failure to scheduling existing tasks for reasons other than already being scheduled', async () => {
+ const taskScheduling = new TaskScheduling(taskSchedulingOpts);
+ mockTaskStore.schedule.mockRejectedValueOnce({
+ statusCode: 500,
+ });
+
+ return expect(
+ taskScheduling.ensureScheduled({
+ id: 'my-foo-id',
+ taskType: 'foo',
+ params: {},
+ state: {},
+ })
+ ).rejects.toMatchObject({
+ statusCode: 500,
+ });
+ });
+
+ test('doesnt allow naively rescheduling existing tasks that have already been scheduled', async () => {
+ const taskScheduling = new TaskScheduling(taskSchedulingOpts);
+ mockTaskStore.schedule.mockRejectedValueOnce({
+ statusCode: 409,
+ });
+
+ return expect(
+ taskScheduling.schedule({
+ id: 'my-foo-id',
+ taskType: 'foo',
+ params: {},
+ state: {},
+ })
+ ).rejects.toMatchObject({
+ statusCode: 409,
+ });
+ });
+
+ describe('runNow', () => {
+ test('resolves when the task run succeeds', () => {
+ const events$ = new Subject();
+ const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
+
+ const taskScheduling = new TaskScheduling({
+ ...taskSchedulingOpts,
+ taskPollingLifecycle: taskPollingLifecycleMock.create({ events$ }),
+ });
+
+ const result = taskScheduling.runNow(id);
+
+ const task = { id } as ConcreteTaskInstance;
+ events$.next(asTaskRunEvent(id, asOk(task)));
+
+ return expect(result).resolves.toEqual({ id });
+ });
+
+ test('rejects when the task run fails', () => {
+ const events$ = new Subject();
+ const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
+
+ const taskScheduling = new TaskScheduling({
+ ...taskSchedulingOpts,
+ taskPollingLifecycle: taskPollingLifecycleMock.create({ events$ }),
+ });
+
+ const result = taskScheduling.runNow(id);
+
+ const task = { id } as ConcreteTaskInstance;
+ events$.next(asTaskClaimEvent(id, asOk(task)));
+ events$.next(asTaskMarkRunningEvent(id, asOk(task)));
+ events$.next(asTaskRunEvent(id, asErr(new Error('some thing gone wrong'))));
+
+ return expect(result).rejects.toMatchInlineSnapshot(
+ `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2": Error: some thing gone wrong]`
+ );
+ });
+
+ test('rejects when the task mark as running fails', () => {
+ const events$ = new Subject();
+ const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
+
+ const taskScheduling = new TaskScheduling({
+ ...taskSchedulingOpts,
+ taskPollingLifecycle: taskPollingLifecycleMock.create({ events$ }),
+ });
+
+ const result = taskScheduling.runNow(id);
+
+ const task = { id } as ConcreteTaskInstance;
+ events$.next(asTaskClaimEvent(id, asOk(task)));
+ events$.next(asTaskMarkRunningEvent(id, asErr(new Error('some thing gone wrong'))));
+
+ return expect(result).rejects.toMatchInlineSnapshot(
+ `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2": Error: some thing gone wrong]`
+ );
+ });
+
+ test('when a task claim fails we ensure the task exists', async () => {
+ const events$ = new Subject();
+ const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
+
+ mockTaskStore.getLifecycle.mockResolvedValue(TaskLifecycleResult.NotFound);
+
+ const taskScheduling = new TaskScheduling({
+ ...taskSchedulingOpts,
+ taskPollingLifecycle: taskPollingLifecycleMock.create({ events$ }),
+ });
+
+ const result = taskScheduling.runNow(id);
+
+ events$.next(asTaskClaimEvent(id, asErr(none)));
+
+ await expect(result).rejects.toEqual(
+ new Error(`Failed to run task "${id}" as it does not exist`)
+ );
+
+ expect(mockTaskStore.getLifecycle).toHaveBeenCalledWith(id);
+ });
+
+ test('when a task claim fails we ensure the task isnt already claimed', async () => {
+ const events$ = new Subject();
+ const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
+
+ mockTaskStore.getLifecycle.mockResolvedValue(TaskStatus.Claiming);
+
+ const taskScheduling = new TaskScheduling({
+ ...taskSchedulingOpts,
+ taskPollingLifecycle: taskPollingLifecycleMock.create({ events$ }),
+ });
+
+ const result = taskScheduling.runNow(id);
+
+ events$.next(asTaskClaimEvent(id, asErr(none)));
+
+ await expect(result).rejects.toEqual(
+ new Error(`Failed to run task "${id}" as it is currently running`)
+ );
+
+ expect(mockTaskStore.getLifecycle).toHaveBeenCalledWith(id);
+ });
+
+ test('when a task claim fails we ensure the task isnt already running', async () => {
+ const events$ = new Subject();
+ const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
+
+ mockTaskStore.getLifecycle.mockResolvedValue(TaskStatus.Running);
+
+ const taskScheduling = new TaskScheduling({
+ ...taskSchedulingOpts,
+ taskPollingLifecycle: taskPollingLifecycleMock.create({ events$ }),
+ });
+
+ const result = taskScheduling.runNow(id);
+
+ events$.next(asTaskClaimEvent(id, asErr(none)));
+
+ await expect(result).rejects.toEqual(
+ new Error(`Failed to run task "${id}" as it is currently running`)
+ );
+
+ expect(mockTaskStore.getLifecycle).toHaveBeenCalledWith(id);
+ });
+
+ test('rejects when the task run fails due to capacity', async () => {
+ const events$ = new Subject();
+ const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
+
+ mockTaskStore.getLifecycle.mockResolvedValue(TaskStatus.Idle);
+
+ const taskScheduling = new TaskScheduling({
+ ...taskSchedulingOpts,
+ taskPollingLifecycle: taskPollingLifecycleMock.create({ events$ }),
+ });
+
+ const result = taskScheduling.runNow(id);
+
+ events$.next(asTaskRunRequestEvent(id, asErr(new Error('failed to buffer request'))));
+
+ await expect(result).rejects.toEqual(
+ new Error(`Failed to run task "${id}": Task Manager is at capacity, please try again later`)
+ );
+ expect(mockTaskStore.getLifecycle).not.toHaveBeenCalled();
+ });
+
+ test('when a task claim fails we return the underlying error if the task is idle', async () => {
+ const events$ = new Subject();
+ const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
+
+ mockTaskStore.getLifecycle.mockResolvedValue(TaskStatus.Idle);
+
+ const taskScheduling = new TaskScheduling({
+ ...taskSchedulingOpts,
+ taskPollingLifecycle: taskPollingLifecycleMock.create({ events$ }),
+ });
+
+ const result = taskScheduling.runNow(id);
+
+ events$.next(asTaskClaimEvent(id, asErr(none)));
+
+ await expect(result).rejects.toMatchInlineSnapshot(
+ `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2" for unknown reason (Current Task Lifecycle is "idle")]`
+ );
+
+ expect(mockTaskStore.getLifecycle).toHaveBeenCalledWith(id);
+ });
+
+ test('when a task claim fails we return the underlying error if the task is failed', async () => {
+ const events$ = new Subject();
+ const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
+
+ mockTaskStore.getLifecycle.mockResolvedValue(TaskStatus.Failed);
+
+ const taskScheduling = new TaskScheduling({
+ ...taskSchedulingOpts,
+ taskPollingLifecycle: taskPollingLifecycleMock.create({ events$ }),
+ });
+
+ const result = taskScheduling.runNow(id);
+
+ events$.next(asTaskClaimEvent(id, asErr(none)));
+
+ await expect(result).rejects.toMatchInlineSnapshot(
+ `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2" for unknown reason (Current Task Lifecycle is "failed")]`
+ );
+
+ expect(mockTaskStore.getLifecycle).toHaveBeenCalledWith(id);
+ });
+
+ test('ignores task run success of other tasks', () => {
+ const events$ = new Subject();
+ const id = '01ddff11-e88a-4d13-bc4e-256164e755e2';
+ const differentTask = '4bebf429-181b-4518-bb7d-b4246d8a35f0';
+
+ const taskScheduling = new TaskScheduling({
+ ...taskSchedulingOpts,
+ taskPollingLifecycle: taskPollingLifecycleMock.create({ events$ }),
+ });
+
+ const result = taskScheduling.runNow(id);
+
+ const task = { id } as ConcreteTaskInstance;
+ const otherTask = { id: differentTask } as ConcreteTaskInstance;
+ events$.next(asTaskClaimEvent(id, asOk(task)));
+ events$.next(asTaskClaimEvent(differentTask, asOk(otherTask)));
+
+ events$.next(asTaskRunEvent(differentTask, asOk(task)));
+
+ events$.next(asTaskRunEvent(id, asErr(new Error('some thing gone wrong'))));
+
+ return expect(result).rejects.toMatchInlineSnapshot(
+ `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2": Error: some thing gone wrong]`
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/task_manager/server/task_scheduling.ts b/x-pack/plugins/task_manager/server/task_scheduling.ts
new file mode 100644
index 00000000000000..00f7d853d71142
--- /dev/null
+++ b/x-pack/plugins/task_manager/server/task_scheduling.ts
@@ -0,0 +1,179 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { filter } from 'rxjs/operators';
+
+import { pipe } from 'fp-ts/lib/pipeable';
+import { Option, map as mapOptional, getOrElse } from 'fp-ts/lib/Option';
+
+import { Logger } from '../../../../src/core/server';
+import { asOk, either, map, mapErr, promiseResult } from './lib/result_type';
+import { isTaskRunEvent, isTaskClaimEvent, isTaskRunRequestEvent } from './task_events';
+import { Middleware } from './lib/middleware';
+import {
+ ConcreteTaskInstance,
+ TaskInstanceWithId,
+ TaskInstanceWithDeprecatedFields,
+ TaskLifecycle,
+ TaskLifecycleResult,
+ TaskStatus,
+} from './task';
+import { TaskStore } from './task_store';
+import { ensureDeprecatedFieldsAreCorrected } from './lib/correct_deprecated_fields';
+import { TaskLifecycleEvent, TaskPollingLifecycle } from './polling_lifecycle';
+
+const VERSION_CONFLICT_STATUS = 409;
+
+export interface TaskSchedulingOpts {
+ logger: Logger;
+ taskStore: TaskStore;
+ taskPollingLifecycle: TaskPollingLifecycle;
+ middleware: Middleware;
+}
+
+interface RunNowResult {
+ id: string;
+}
+
+export class TaskScheduling {
+ private store: TaskStore;
+ private taskPollingLifecycle: TaskPollingLifecycle;
+ private logger: Logger;
+ private middleware: Middleware;
+
+ /**
+ * Initializes the task manager, preventing any further addition of middleware,
+ * enabling the task manipulation methods, and beginning the background polling
+ * mechanism.
+ */
+ constructor(opts: TaskSchedulingOpts) {
+ this.logger = opts.logger;
+ this.middleware = opts.middleware;
+ this.taskPollingLifecycle = opts.taskPollingLifecycle;
+ this.store = opts.taskStore;
+ }
+
+ /**
+ * Schedules a task.
+ *
+ * @param task - The task being scheduled.
+ * @returns {Promise}
+ */
+ public async schedule(
+ taskInstance: TaskInstanceWithDeprecatedFields,
+ options?: Record
+ ): Promise {
+ const { taskInstance: modifiedTask } = await this.middleware.beforeSave({
+ ...options,
+ taskInstance: ensureDeprecatedFieldsAreCorrected(taskInstance, this.logger),
+ });
+ return await this.store.schedule(modifiedTask);
+ }
+
+ /**
+ * Run task.
+ *
+ * @param taskId - The task being scheduled.
+ * @returns {Promise}
+ */
+ public async runNow(taskId: string): Promise {
+ return new Promise(async (resolve, reject) => {
+ this.awaitTaskRunResult(taskId).then(resolve).catch(reject);
+ this.taskPollingLifecycle.attemptToRun(taskId);
+ });
+ }
+
+ /**
+ * Schedules a task with an Id
+ *
+ * @param task - The task being scheduled.
+ * @returns {Promise}
+ */
+ public async ensureScheduled(
+ taskInstance: TaskInstanceWithId,
+ options?: Record
+ ): Promise {
+ try {
+ return await this.schedule(taskInstance, options);
+ } catch (err) {
+ if (err.statusCode === VERSION_CONFLICT_STATUS) {
+ return taskInstance;
+ }
+ throw err;
+ }
+ }
+
+ private async awaitTaskRunResult(taskId: string): Promise {
+ return new Promise((resolve, reject) => {
+ const subscription = this.taskPollingLifecycle.events
+ // listen for all events related to the current task
+ .pipe(filter(({ id }: TaskLifecycleEvent) => id === taskId))
+ .subscribe((taskEvent: TaskLifecycleEvent) => {
+ if (isTaskClaimEvent(taskEvent)) {
+ mapErr(async (error: Option) => {
+ // reject if any error event takes place for the requested task
+ subscription.unsubscribe();
+ return reject(await this.identifyTaskFailureReason(taskId, error));
+ }, taskEvent.event);
+ } else {
+ either>(
+ taskEvent.event,
+ (taskInstance: ConcreteTaskInstance) => {
+ // resolve if the task has run sucessfully
+ if (isTaskRunEvent(taskEvent)) {
+ subscription.unsubscribe();
+ resolve({ id: taskInstance.id });
+ }
+ },
+ async (error: Error | Option) => {
+ // reject if any error event takes place for the requested task
+ subscription.unsubscribe();
+ return reject(
+ new Error(
+ `Failed to run task "${taskId}": ${
+ isTaskRunRequestEvent(taskEvent)
+ ? `Task Manager is at capacity, please try again later`
+ : error
+ }`
+ )
+ );
+ }
+ );
+ }
+ });
+ });
+ }
+
+ private async identifyTaskFailureReason(taskId: string, error: Option) {
+ return map(
+ await pipe(
+ error,
+ mapOptional(async (taskReturnedBySweep) => asOk(taskReturnedBySweep.status)),
+ getOrElse(() =>
+ // if the error happened in the Claim phase - we try to provide better insight
+ // into why we failed to claim by getting the task's current lifecycle status
+ promiseResult(this.store.getLifecycle(taskId))
+ )
+ ),
+ (taskLifecycleStatus: TaskLifecycle) => {
+ if (taskLifecycleStatus === TaskLifecycleResult.NotFound) {
+ return new Error(`Failed to run task "${taskId}" as it does not exist`);
+ } else if (
+ taskLifecycleStatus === TaskStatus.Running ||
+ taskLifecycleStatus === TaskStatus.Claiming
+ ) {
+ return new Error(`Failed to run task "${taskId}" as it is currently running`);
+ }
+ return new Error(
+ `Failed to run task "${taskId}" for unknown reason (Current Task Lifecycle is "${taskLifecycleStatus}")`
+ );
+ },
+ (getLifecycleError: Error) =>
+ new Error(
+ `Failed to run task "${taskId}" and failed to get current Status:${getLifecycleError}`
+ )
+ );
+ }
+}
diff --git a/x-pack/plugins/task_manager/server/task_store.mock.ts b/x-pack/plugins/task_manager/server/task_store.mock.ts
index 86db695bc5e2cb..9b82a3e3ee7ab3 100644
--- a/x-pack/plugins/task_manager/server/task_store.mock.ts
+++ b/x-pack/plugins/task_manager/server/task_store.mock.ts
@@ -4,15 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { Observable, Subject } from 'rxjs';
+import { TaskClaim } from './task_events';
+
import { TaskStore } from './task_store';
interface TaskStoreOptions {
maxAttempts?: number;
index?: string;
taskManagerId?: string;
+ events?: Observable;
}
export const taskStoreMock = {
- create({ maxAttempts = 0, index = '', taskManagerId = '' }: TaskStoreOptions) {
+ create({
+ maxAttempts = 0,
+ index = '',
+ taskManagerId = '',
+ events = new Subject(),
+ }: TaskStoreOptions) {
const mocked = ({
update: jest.fn(),
remove: jest.fn(),
@@ -25,6 +34,7 @@ export const taskStoreMock = {
maxAttempts,
index,
taskManagerId,
+ events,
} as unknown) as jest.Mocked;
return mocked;
},
diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts
index 5a3ee12d593c9c..8d47d3dd30b825 100644
--- a/x-pack/plugins/task_manager/server/task_store.test.ts
+++ b/x-pack/plugins/task_manager/server/task_store.test.ts
@@ -5,20 +5,18 @@
*/
import _ from 'lodash';
-import sinon from 'sinon';
import uuid from 'uuid';
import { filter, take, first } from 'rxjs/operators';
import { Option, some, none } from 'fp-ts/lib/Option';
import {
- TaskDictionary,
- TaskDefinition,
TaskInstance,
TaskStatus,
TaskLifecycleResult,
SerializedConcreteTaskInstance,
ConcreteTaskInstance,
} from './task';
+import { elasticsearchServiceMock } from '../../../../src/core/server/mocks';
import { StoreOpts, OwnershipClaimingOpts, TaskStore, SearchOpts } from './task_store';
import { savedObjectsRepositoryMock } from 'src/core/server/mocks';
import {
@@ -29,24 +27,11 @@ import {
} from 'src/core/server';
import { asTaskClaimEvent, TaskEvent } from './task_events';
import { asOk, asErr } from './lib/result_type';
-
-const taskDefinitions: TaskDictionary = {
- report: {
- type: 'report',
- title: '',
- createTaskRunner: jest.fn(),
- },
- dernstraight: {
- type: 'dernstraight',
- title: '',
- createTaskRunner: jest.fn(),
- },
- yawn: {
- type: 'yawn',
- title: '',
- createTaskRunner: jest.fn(),
- },
-};
+import { TaskTypeDictionary } from './task_type_dictionary';
+import { RequestEvent } from '@elastic/elasticsearch/lib/Transport';
+import { Search, UpdateByQuery } from '@elastic/elasticsearch/api/requestParams';
+import { BoolClauseWithAnyCondition, TermFilter } from './queries/query_clauses';
+import { mockLogger } from './test_utils';
const savedObjectsClient = savedObjectsRepositoryMock.create();
const serializer = new SavedObjectsSerializer(new SavedObjectTypeRegistry());
@@ -64,6 +49,22 @@ const mockedDate = new Date('2019-02-12T21:01:22.479Z');
}
};
+const taskDefinitions = new TaskTypeDictionary(mockLogger());
+taskDefinitions.registerTaskDefinitions({
+ report: {
+ title: 'report',
+ createTaskRunner: jest.fn(),
+ },
+ dernstraight: {
+ title: 'dernstraight',
+ createTaskRunner: jest.fn(),
+ },
+ yawn: {
+ title: 'yawn',
+ createTaskRunner: jest.fn(),
+ },
+});
+
describe('TaskStore', () => {
describe('schedule', () => {
let store: TaskStore;
@@ -73,7 +74,7 @@ describe('TaskStore', () => {
index: 'tasky',
taskManagerId: '',
serializer,
- callCluster: jest.fn(),
+ esClient: elasticsearchServiceMock.createClusterClient().asInternalUser,
maxAttempts: 2,
definitions: taskDefinitions,
savedObjectsRepository: savedObjectsClient,
@@ -198,14 +199,15 @@ describe('TaskStore', () => {
describe('fetch', () => {
let store: TaskStore;
- const callCluster = jest.fn();
+ let esClient: ReturnType['asInternalUser'];
beforeAll(() => {
+ esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
store = new TaskStore({
index: 'tasky',
taskManagerId: '',
serializer,
- callCluster,
+ esClient,
maxAttempts: 2,
definitions: taskDefinitions,
savedObjectsRepository: savedObjectsClient,
@@ -213,16 +215,15 @@ describe('TaskStore', () => {
});
async function testFetch(opts?: SearchOpts, hits: unknown[] = []) {
- callCluster.mockResolvedValue({ hits: { hits } });
+ esClient.search.mockResolvedValue(asApiResponse({ hits: { hits } }));
const result = await store.fetch(opts);
- expect(callCluster).toHaveBeenCalledTimes(1);
- expect(callCluster).toHaveBeenCalledWith('search', expect.anything());
+ expect(esClient.search).toHaveBeenCalledTimes(1);
return {
result,
- args: callCluster.mock.calls[0][1],
+ args: esClient.search.mock.calls[0][0],
};
}
@@ -257,7 +258,7 @@ describe('TaskStore', () => {
test('pushes error from call cluster to errors$', async () => {
const firstErrorPromise = store.errors$.pipe(first()).toPromise();
- callCluster.mockRejectedValue(new Error('Failure'));
+ esClient.search.mockRejectedValue(new Error('Failure'));
await expect(store.fetch()).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`);
expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`);
});
@@ -274,17 +275,18 @@ describe('TaskStore', () => {
claimingOpts: OwnershipClaimingOpts;
}) {
const versionConflicts = 2;
- const callCluster = sinon.spy(async (name: string, params?: unknown) =>
- name === 'updateByQuery'
- ? {
- total: hits.length + versionConflicts,
- updated: hits.length,
- version_conflicts: versionConflicts,
- }
- : { hits: { hits } }
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ esClient.search.mockResolvedValue(asApiResponse({ hits: { hits } }));
+ esClient.updateByQuery.mockResolvedValue(
+ asApiResponse({
+ total: hits.length + versionConflicts,
+ updated: hits.length,
+ version_conflicts: versionConflicts,
+ })
);
+
const store = new TaskStore({
- callCluster,
+ esClient,
maxAttempts: 2,
definitions: taskDefinitions,
serializer,
@@ -296,26 +298,41 @@ describe('TaskStore', () => {
const result = await store.claimAvailableTasks(claimingOpts);
- sinon.assert.calledTwice(callCluster);
- sinon.assert.calledWithMatch(callCluster, 'updateByQuery', { max_docs: claimingOpts.size });
- sinon.assert.calledWithMatch(callCluster, 'search', { body: { size: claimingOpts.size } });
-
+ expect(esClient.updateByQuery.mock.calls[0][0]).toMatchObject({
+ max_docs: claimingOpts.size,
+ });
+ expect(esClient.search.mock.calls[0][0]).toMatchObject({ body: { size: claimingOpts.size } });
return {
result,
- args: Object.assign({}, ...callCluster.args.map(([name, args]) => ({ [name]: args }))),
+ args: {
+ search: esClient.search.mock.calls[0][0]! as Search<{
+ query: BoolClauseWithAnyCondition;
+ size: number;
+ sort: string | string[];
+ }>,
+ updateByQuery: esClient.updateByQuery.mock.calls[0][0]! as UpdateByQuery<{
+ query: BoolClauseWithAnyCondition;
+ size: number;
+ sort: string | string[];
+ script: object;
+ }>,
+ },
};
}
test('it returns normally with no tasks when the index does not exist.', async () => {
- const callCluster = sinon.spy(async (name: string, params?: unknown) => ({
- total: 0,
- updated: 0,
- }));
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ esClient.updateByQuery.mockResolvedValue(
+ asApiResponse({
+ total: 0,
+ updated: 0,
+ })
+ );
const store = new TaskStore({
index: 'tasky',
taskManagerId: '',
serializer,
- callCluster,
+ esClient,
definitions: taskDefinitions,
maxAttempts: 2,
savedObjectsRepository: savedObjectsClient,
@@ -324,9 +341,8 @@ describe('TaskStore', () => {
claimOwnershipUntil: new Date(),
size: 10,
});
- sinon.assert.calledOnce(callCluster);
- sinon.assert.calledWithMatch(callCluster, 'updateByQuery', {
- ignoreUnavailable: true,
+ expect(esClient.updateByQuery.mock.calls[0][0]).toMatchObject({
+ ignore_unavailable: true,
max_docs: 10,
});
expect(docs.length).toBe(0);
@@ -335,28 +351,28 @@ describe('TaskStore', () => {
test('it filters claimed tasks down by supported types, maxAttempts, status, and runAt', async () => {
const maxAttempts = _.random(2, 43);
const customMaxAttempts = _.random(44, 100);
+
+ const definitions = new TaskTypeDictionary(mockLogger());
+ definitions.registerTaskDefinitions({
+ foo: {
+ title: 'foo',
+ createTaskRunner: jest.fn(),
+ },
+ bar: {
+ title: 'bar',
+ maxAttempts: customMaxAttempts,
+ createTaskRunner: jest.fn(),
+ },
+ });
+
const {
args: {
- updateByQuery: {
- body: { query },
- },
+ updateByQuery: { body: { query } = {} },
},
} = await testClaimAvailableTasks({
opts: {
maxAttempts,
- definitions: {
- foo: {
- type: 'foo',
- title: '',
- createTaskRunner: jest.fn(),
- },
- bar: {
- type: 'bar',
- title: '',
- maxAttempts: customMaxAttempts,
- createTaskRunner: jest.fn(),
- },
- },
+ definitions,
},
claimingOpts: { claimOwnershipUntil: new Date(), size: 10 },
});
@@ -465,28 +481,26 @@ describe('TaskStore', () => {
test('it supports claiming specific tasks by id', async () => {
const maxAttempts = _.random(2, 43);
const customMaxAttempts = _.random(44, 100);
+ const definitions = new TaskTypeDictionary(mockLogger());
+ definitions.registerTaskDefinitions({
+ foo: {
+ title: 'foo',
+ createTaskRunner: jest.fn(),
+ },
+ bar: {
+ title: 'bar',
+ maxAttempts: customMaxAttempts,
+ createTaskRunner: jest.fn(),
+ },
+ });
const {
args: {
- updateByQuery: {
- body: { query, sort },
- },
+ updateByQuery: { body: { query, sort } = {} },
},
} = await testClaimAvailableTasks({
opts: {
maxAttempts,
- definitions: {
- foo: {
- type: 'foo',
- title: '',
- createTaskRunner: jest.fn(),
- },
- bar: {
- type: 'bar',
- title: '',
- maxAttempts: customMaxAttempts,
- createTaskRunner: jest.fn(),
- },
- },
+ definitions,
},
claimingOpts: {
claimOwnershipUntil: new Date(),
@@ -634,9 +648,7 @@ if (doc['task.runAt'].size()!=0) {
const claimOwnershipUntil = new Date(Date.now());
const {
args: {
- updateByQuery: {
- body: { script },
- },
+ updateByQuery: { body: { script } = {} },
},
} = await testClaimAvailableTasks({
opts: {
@@ -710,9 +722,7 @@ if (doc['task.runAt'].size()!=0) {
const {
result: { docs },
args: {
- search: {
- body: { query },
- },
+ search: { body: { query } = {} },
},
} = await testClaimAvailableTasks({
opts: {
@@ -725,7 +735,7 @@ if (doc['task.runAt'].size()!=0) {
hits: tasks,
});
- expect(query.bool.must).toContainEqual({
+ expect(query?.bool?.must).toContainEqual({
bool: {
must: [
{
@@ -804,11 +814,9 @@ if (doc['task.runAt'].size()!=0) {
},
];
const {
- result: { docs },
+ result: { docs } = {},
args: {
- search: {
- body: { query },
- },
+ search: { body: { query } = {} },
},
} = await testClaimAvailableTasks({
opts: {
@@ -821,7 +829,7 @@ if (doc['task.runAt'].size()!=0) {
hits: tasks,
});
- expect(query.bool.must).toContainEqual({
+ expect(query?.bool?.must).toContainEqual({
bool: {
must: [
{
@@ -900,11 +908,9 @@ if (doc['task.runAt'].size()!=0) {
},
];
const {
- result: { docs },
+ result: { docs } = {},
args: {
- search: {
- body: { query },
- },
+ search: { body: { query } = {} },
},
} = await testClaimAvailableTasks({
opts: {
@@ -917,7 +923,7 @@ if (doc['task.runAt'].size()!=0) {
hits: tasks,
});
- expect(query.bool.must).toContainEqual({
+ expect(query?.bool?.must).toContainEqual({
bool: {
must: [
{
@@ -961,19 +967,19 @@ if (doc['task.runAt'].size()!=0) {
});
test('pushes error from saved objects client to errors$', async () => {
- const callCluster = jest.fn();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
const store = new TaskStore({
index: 'tasky',
taskManagerId: '',
serializer,
- callCluster,
+ esClient,
definitions: taskDefinitions,
maxAttempts: 2,
savedObjectsRepository: savedObjectsClient,
});
const firstErrorPromise = store.errors$.pipe(first()).toPromise();
- callCluster.mockRejectedValue(new Error('Failure'));
+ esClient.updateByQuery.mockRejectedValue(new Error('Failure'));
await expect(
store.claimAvailableTasks({
claimOwnershipUntil: new Date(),
@@ -986,13 +992,15 @@ if (doc['task.runAt'].size()!=0) {
describe('update', () => {
let store: TaskStore;
+ let esClient: ReturnType['asInternalUser'];
beforeAll(() => {
+ esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
store = new TaskStore({
index: 'tasky',
taskManagerId: '',
serializer,
- callCluster: jest.fn(),
+ esClient,
maxAttempts: 2,
definitions: taskDefinitions,
savedObjectsRepository: savedObjectsClient,
@@ -1092,7 +1100,7 @@ if (doc['task.runAt'].size()!=0) {
index: 'tasky',
taskManagerId: '',
serializer,
- callCluster: jest.fn(),
+ esClient: elasticsearchServiceMock.createClusterClient().asInternalUser,
maxAttempts: 2,
definitions: taskDefinitions,
savedObjectsRepository: savedObjectsClient,
@@ -1132,7 +1140,7 @@ if (doc['task.runAt'].size()!=0) {
index: 'tasky',
taskManagerId: '',
serializer,
- callCluster: jest.fn(),
+ esClient: elasticsearchServiceMock.createClusterClient().asInternalUser,
maxAttempts: 2,
definitions: taskDefinitions,
savedObjectsRepository: savedObjectsClient,
@@ -1140,17 +1148,18 @@ if (doc['task.runAt'].size()!=0) {
});
test('removes the task with the specified id', async () => {
- const id = `id-${_.random(1, 20)}`;
+ const id = randomId();
const result = await store.remove(id);
expect(result).toBeUndefined();
expect(savedObjectsClient.delete).toHaveBeenCalledWith('task', id);
});
test('pushes error from saved objects client to errors$', async () => {
- const id = `id-${_.random(1, 20)}`;
const firstErrorPromise = store.errors$.pipe(first()).toPromise();
savedObjectsClient.delete.mockRejectedValue(new Error('Failure'));
- await expect(store.remove(id)).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`);
+ await expect(store.remove(randomId())).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Failure"`
+ );
expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`);
});
});
@@ -1163,7 +1172,7 @@ if (doc['task.runAt'].size()!=0) {
index: 'tasky',
taskManagerId: '',
serializer,
- callCluster: jest.fn(),
+ esClient: elasticsearchServiceMock.createClusterClient().asInternalUser,
maxAttempts: 2,
definitions: taskDefinitions,
savedObjectsRepository: savedObjectsClient,
@@ -1171,13 +1180,12 @@ if (doc['task.runAt'].size()!=0) {
});
test('gets the task with the specified id', async () => {
- const id = `id-${_.random(1, 20)}`;
const task = {
runAt: mockedDate,
scheduledAt: mockedDate,
startedAt: null,
retryAt: null,
- id,
+ id: randomId(),
params: { hello: 'world' },
state: { foo: 'bar' },
taskType: 'report',
@@ -1198,18 +1206,17 @@ if (doc['task.runAt'].size()!=0) {
version: '123',
}));
- const result = await store.get(id);
+ const result = await store.get(task.id);
expect(result).toEqual(task);
- expect(savedObjectsClient.get).toHaveBeenCalledWith('task', id);
+ expect(savedObjectsClient.get).toHaveBeenCalledWith('task', task.id);
});
test('pushes error from saved objects client to errors$', async () => {
- const id = `id-${_.random(1, 20)}`;
const firstErrorPromise = store.errors$.pipe(first()).toPromise();
savedObjectsClient.get.mockRejectedValue(new Error('Failure'));
- await expect(store.get(id)).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`);
+ await expect(store.get(randomId())).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`);
expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`);
});
});
@@ -1219,13 +1226,12 @@ if (doc['task.runAt'].size()!=0) {
expect.assertions(4);
return Promise.all(
Object.values(TaskStatus).map(async (status) => {
- const id = `id-${_.random(1, 20)}`;
const task = {
runAt: mockedDate,
scheduledAt: mockedDate,
startedAt: null,
retryAt: null,
- id,
+ id: randomId(),
params: { hello: 'world' },
state: { foo: 'bar' },
taskType: 'report',
@@ -1235,7 +1241,6 @@ if (doc['task.runAt'].size()!=0) {
ownerId: null,
};
- const callCluster = jest.fn();
savedObjectsClient.get.mockImplementation(async (type: string, objectId: string) => ({
id: objectId,
type,
@@ -1251,20 +1256,18 @@ if (doc['task.runAt'].size()!=0) {
index: 'tasky',
taskManagerId: '',
serializer,
- callCluster,
+ esClient: elasticsearchServiceMock.createClusterClient().asInternalUser,
maxAttempts: 2,
definitions: taskDefinitions,
savedObjectsRepository: savedObjectsClient,
});
- expect(await store.getLifecycle(id)).toEqual(status);
+ expect(await store.getLifecycle(task.id)).toEqual(status);
})
);
});
test('returns NotFound status if the task doesnt exists ', async () => {
- const id = `id-${_.random(1, 20)}`;
-
savedObjectsClient.get.mockRejectedValueOnce(
SavedObjectsErrorHelpers.createGenericNotFoundError('type', 'id')
);
@@ -1273,18 +1276,16 @@ if (doc['task.runAt'].size()!=0) {
index: 'tasky',
taskManagerId: '',
serializer,
- callCluster: jest.fn(),
+ esClient: elasticsearchServiceMock.createClusterClient().asInternalUser,
maxAttempts: 2,
definitions: taskDefinitions,
savedObjectsRepository: savedObjectsClient,
});
- expect(await store.getLifecycle(id)).toEqual(TaskLifecycleResult.NotFound);
+ expect(await store.getLifecycle(randomId())).toEqual(TaskLifecycleResult.NotFound);
});
test('throws if an unknown error takes place ', async () => {
- const id = `id-${_.random(1, 20)}`;
-
savedObjectsClient.get.mockRejectedValueOnce(
SavedObjectsErrorHelpers.createBadRequestError()
);
@@ -1293,13 +1294,13 @@ if (doc['task.runAt'].size()!=0) {
index: 'tasky',
taskManagerId: '',
serializer,
- callCluster: jest.fn(),
+ esClient: elasticsearchServiceMock.createClusterClient().asInternalUser,
maxAttempts: 2,
definitions: taskDefinitions,
savedObjectsRepository: savedObjectsClient,
});
- return expect(store.getLifecycle(id)).rejects.toThrow('Bad Request');
+ return expect(store.getLifecycle(randomId())).rejects.toThrow('Bad Request');
});
});
@@ -1385,18 +1386,20 @@ if (doc['task.runAt'].size()!=0) {
return { taskManagerId, runAt, tasks };
}
- test('emits an event when a task is succesfully claimed by id', async () => {
+ function instantiateStoreWithMockedApiResponses() {
const { taskManagerId, runAt, tasks } = generateTasks();
- const callCluster = sinon.spy(async (name: string, params?: unknown) =>
- name === 'updateByQuery'
- ? {
- total: tasks.length,
- updated: tasks.length,
- }
- : { hits: { hits: tasks } }
+
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ esClient.search.mockResolvedValue(asApiResponse({ hits: { hits: tasks } }));
+ esClient.updateByQuery.mockResolvedValue(
+ asApiResponse({
+ total: tasks.length,
+ updated: tasks.length,
+ })
);
+
const store = new TaskStore({
- callCluster,
+ esClient,
maxAttempts: 2,
definitions: taskDefinitions,
serializer,
@@ -1405,6 +1408,12 @@ if (doc['task.runAt'].size()!=0) {
index: '',
});
+ return { taskManagerId, runAt, store };
+ }
+
+ test('emits an event when a task is succesfully claimed by id', async () => {
+ const { taskManagerId, runAt, store } = instantiateStoreWithMockedApiResponses();
+
const promise = store.events
.pipe(
filter(
@@ -1446,24 +1455,7 @@ if (doc['task.runAt'].size()!=0) {
});
test('emits an event when a task is succesfully by scheduling', async () => {
- const { taskManagerId, runAt, tasks } = generateTasks();
- const callCluster = sinon.spy(async (name: string, params?: unknown) =>
- name === 'updateByQuery'
- ? {
- total: tasks.length,
- updated: tasks.length,
- }
- : { hits: { hits: tasks } }
- );
- const store = new TaskStore({
- callCluster,
- maxAttempts: 2,
- definitions: taskDefinitions,
- serializer,
- savedObjectsRepository: savedObjectsClient,
- taskManagerId,
- index: '',
- });
+ const { taskManagerId, runAt, store } = instantiateStoreWithMockedApiResponses();
const promise = store.events
.pipe(
@@ -1506,24 +1498,7 @@ if (doc['task.runAt'].size()!=0) {
});
test('emits an event when the store fails to claim a required task by id', async () => {
- const { taskManagerId, runAt, tasks } = generateTasks();
- const callCluster = sinon.spy(async (name: string, params?: unknown) =>
- name === 'updateByQuery'
- ? {
- total: tasks.length,
- updated: tasks.length,
- }
- : { hits: { hits: tasks } }
- );
- const store = new TaskStore({
- callCluster,
- maxAttempts: 2,
- definitions: taskDefinitions,
- serializer,
- savedObjectsRepository: savedObjectsClient,
- taskManagerId,
- index: '',
- });
+ const { taskManagerId, runAt, store } = instantiateStoreWithMockedApiResponses();
const promise = store.events
.pipe(
@@ -1568,24 +1543,7 @@ if (doc['task.runAt'].size()!=0) {
});
test('emits an event when the store fails to find a task which was required by id', async () => {
- const { taskManagerId, tasks } = generateTasks();
- const callCluster = sinon.spy(async (name: string, params?: unknown) =>
- name === 'updateByQuery'
- ? {
- total: tasks.length,
- updated: tasks.length,
- }
- : { hits: { hits: tasks } }
- );
- const store = new TaskStore({
- callCluster,
- maxAttempts: 2,
- definitions: taskDefinitions,
- serializer,
- savedObjectsRepository: savedObjectsClient,
- taskManagerId,
- index: '',
- });
+ const { store } = instantiateStoreWithMockedApiResponses();
const promise = store.events
.pipe(
@@ -1621,3 +1579,10 @@ function generateFakeTasks(count: number = 1) {
sort: ['a', _.random(1, 5)],
}));
}
+
+const asApiResponse = (body: T): RequestEvent =>
+ ({
+ body,
+ } as RequestEvent);
+
+const randomId = () => `id-${_.random(1, 20)}`;
diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts
index 15261be3d89ae5..4c41be9577ad00 100644
--- a/x-pack/plugins/task_manager/server/task_store.ts
+++ b/x-pack/plugins/task_manager/server/task_store.ts
@@ -20,15 +20,13 @@ import {
SavedObjectsRawDoc,
ISavedObjectsRepository,
SavedObjectsUpdateResponse,
+ ElasticsearchClient,
} from '../../../../src/core/server';
import { asOk, asErr, Result } from './lib/result_type';
import {
ConcreteTaskInstance,
- ElasticJs,
- TaskDefinition,
- TaskDictionary,
TaskInstance,
TaskLifecycle,
TaskLifecycleResult,
@@ -60,13 +58,14 @@ import {
SortByRunAtAndRetryAt,
tasksClaimedByOwner,
} from './queries/mark_available_tasks_as_claimed';
+import { TaskTypeDictionary } from './task_type_dictionary';
export interface StoreOpts {
- callCluster: ElasticJs;
+ esClient: ElasticsearchClient;
index: string;
taskManagerId: string;
maxAttempts: number;
- definitions: TaskDictionary;
+ definitions: TaskTypeDictionary;
savedObjectsRepository: ISavedObjectsRepository;
serializer: SavedObjectsSerializer;
}
@@ -123,8 +122,8 @@ export class TaskStore {
public readonly taskManagerId: string;
public readonly errors$ = new Subject();
- private callCluster: ElasticJs;
- private definitions: TaskDictionary;
+ private esClient: ElasticsearchClient;
+ private definitions: TaskTypeDictionary;
private savedObjectsRepository: ISavedObjectsRepository;
private serializer: SavedObjectsSerializer;
private events$: Subject;
@@ -132,7 +131,7 @@ export class TaskStore {
/**
* Constructs a new TaskStore.
* @param {StoreOpts} opts
- * @prop {CallCluster} callCluster - The elastic search connection
+ * @prop {esClient} esClient - An elasticsearch client
* @prop {string} index - The name of the task manager index
* @prop {number} maxAttempts - The maximum number of attempts before a task will be abandoned
* @prop {TaskDefinition} definition - The definition of the task being run
@@ -140,7 +139,7 @@ export class TaskStore {
* @prop {savedObjectsRepository} - An instance to the saved objects repository
*/
constructor(opts: StoreOpts) {
- this.callCluster = opts.callCluster;
+ this.esClient = opts.esClient;
this.index = opts.index;
this.taskManagerId = opts.taskManagerId;
this.maxAttempts = opts.maxAttempts;
@@ -164,13 +163,7 @@ export class TaskStore {
* @param task - The task being scheduled.
*/
public async schedule(taskInstance: TaskInstance): Promise {
- if (!this.definitions[taskInstance.taskType]) {
- throw new Error(
- `Unsupported task type "${taskInstance.taskType}". Supported types are ${Object.keys(
- this.definitions
- ).join(', ')}`
- );
- }
+ this.definitions.ensureHas(taskInstance.taskType);
let savedObject;
try {
@@ -265,6 +258,9 @@ export class TaskStore {
claimTasksById: OwnershipClaimingOpts['claimTasksById'],
size: OwnershipClaimingOpts['size']
): Promise {
+ const tasksWithRemainingAttempts = [...this.definitions].map(([type, { maxAttempts }]) =>
+ taskWithLessThanMaxAttempts(type, maxAttempts || this.maxAttempts)
+ );
const queryForScheduledTasks = mustBeAllOf(
// Either a task with idle status and runAt <= now or
// status running or claiming with a retryAt <= now.
@@ -272,9 +268,7 @@ export class TaskStore {
// Either task has a schedule or the attempts < the maximum configured
shouldBeOneOf(
TaskWithSchedule,
- ...Object.entries(this.definitions).map(([type, { maxAttempts }]) =>
- taskWithLessThanMaxAttempts(type, maxAttempts || this.maxAttempts)
- )
+ ...tasksWithRemainingAttempts
)
);
@@ -473,30 +467,31 @@ export class TaskStore {
private async search(opts: SearchOpts = {}): Promise {
const { query } = ensureQueryOnlyReturnsTaskObjects(opts);
- let result;
try {
- result = await this.callCluster('search', {
+ const {
+ body: {
+ hits: { hits: tasks },
+ },
+ } = await this.esClient.search>({
index: this.index,
- ignoreUnavailable: true,
+ ignore_unavailable: true,
body: {
...opts,
query,
},
});
+
+ return {
+ docs: tasks
+ .filter((doc) => this.serializer.isRawSavedObject(doc))
+ .map((doc) => this.serializer.rawToSavedObject(doc))
+ .map((doc) => omit(doc, 'namespace') as SavedObject)
+ .map(savedObjectToConcreteTaskInstance),
+ };
} catch (e) {
this.errors$.next(e);
throw e;
}
-
- const rawDocs = (result as SearchResponse).hits.hits;
-
- return {
- docs: (rawDocs as SavedObjectsRawDoc[])
- .filter((doc) => this.serializer.isRawSavedObject(doc))
- .map((doc) => this.serializer.rawToSavedObject(doc))
- .map((doc) => omit(doc, 'namespace') as SavedObject)
- .map(savedObjectToConcreteTaskInstance),
- };
}
private async updateByQuery(
@@ -505,11 +500,13 @@ export class TaskStore {
{ max_docs }: UpdateByQueryOpts = {}
): Promise {
const { query } = ensureQueryOnlyReturnsTaskObjects(opts);
- let result;
try {
- result = await this.callCluster('updateByQuery', {
+ const {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ body: { total, updated, version_conflicts },
+ } = await this.esClient.updateByQuery({
index: this.index,
- ignoreUnavailable: true,
+ ignore_unavailable: true,
refresh: true,
max_docs,
conflicts: 'proceed',
@@ -518,18 +515,16 @@ export class TaskStore {
query,
},
});
+
+ return {
+ total,
+ updated,
+ version_conflicts,
+ };
} catch (e) {
this.errors$.next(e);
throw e;
}
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const { total, updated, version_conflicts } = result as UpdateDocumentByQueryResponse;
- return {
- total,
- updated,
- version_conflicts,
- };
}
}
diff --git a/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts b/x-pack/plugins/task_manager/server/task_type_dictionary.test.ts
similarity index 66%
rename from x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts
rename to x-pack/plugins/task_manager/server/task_type_dictionary.test.ts
index 650eb36347c862..e1d6ef17f5f9d1 100644
--- a/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts
+++ b/x-pack/plugins/task_manager/server/task_type_dictionary.test.ts
@@ -5,8 +5,8 @@
*/
import { get } from 'lodash';
-import { RunContext, TaskDictionary, TaskDefinition } from '../task';
-import { sanitizeTaskDefinitions } from './sanitize_task_definitions';
+import { RunContext, TaskDefinition } from './task';
+import { sanitizeTaskDefinitions } from './task_type_dictionary';
interface Opts {
numTasks: number;
@@ -35,39 +35,40 @@ const getMockTaskDefinitions = (opts: Opts) => {
},
};
}
- return (tasks as unknown) as TaskDictionary;
+ return (tasks as unknown) as Record;
};
-describe('sanitizeTaskDefinitions', () => {
+describe('taskTypeDictionary', () => {
+ describe('sanitizeTaskDefinitions', () => {});
it('provides tasks with defaults', () => {
const taskDefinitions = getMockTaskDefinitions({ numTasks: 3 });
const result = sanitizeTaskDefinitions(taskDefinitions);
expect(result).toMatchInlineSnapshot(`
-Object {
- "test_task_type_0": Object {
- "createTaskRunner": [Function],
- "description": "one super cool task",
- "timeout": "5m",
- "title": "Test",
- "type": "test_task_type_0",
- },
- "test_task_type_1": Object {
- "createTaskRunner": [Function],
- "description": "one super cool task",
- "timeout": "5m",
- "title": "Test",
- "type": "test_task_type_1",
- },
- "test_task_type_2": Object {
- "createTaskRunner": [Function],
- "description": "one super cool task",
- "timeout": "5m",
- "title": "Test",
- "type": "test_task_type_2",
- },
-}
-`);
+ Array [
+ Object {
+ "createTaskRunner": [Function],
+ "description": "one super cool task",
+ "timeout": "5m",
+ "title": "Test",
+ "type": "test_task_type_0",
+ },
+ Object {
+ "createTaskRunner": [Function],
+ "description": "one super cool task",
+ "timeout": "5m",
+ "title": "Test",
+ "type": "test_task_type_1",
+ },
+ Object {
+ "createTaskRunner": [Function],
+ "description": "one super cool task",
+ "timeout": "5m",
+ "title": "Test",
+ "type": "test_task_type_2",
+ },
+ ]
+ `);
});
it('throws a validation exception for invalid task definition', () => {
diff --git a/x-pack/plugins/task_manager/server/task_type_dictionary.ts b/x-pack/plugins/task_manager/server/task_type_dictionary.ts
new file mode 100644
index 00000000000000..cb7cda6dfa845e
--- /dev/null
+++ b/x-pack/plugins/task_manager/server/task_type_dictionary.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import Joi from 'joi';
+import { TaskDefinition, validateTaskDefinition } from './task';
+import { Logger } from '../../../../src/core/server';
+
+/*
+ * The TaskManager is the public interface into the task manager system. This glues together
+ * all of the disparate modules in one integration point. The task manager operates in two different ways:
+ *
+ * - pre-init, it allows middleware registration, but disallows task manipulation
+ * - post-init, it disallows middleware registration, but allows task manipulation
+ *
+ * Due to its complexity, this is mostly tested by integration tests (see readme).
+ */
+
+/**
+ * The public interface into the task manager system.
+ */
+export class TaskTypeDictionary {
+ private definitions = new Map();
+ private logger: Logger;
+
+ constructor(logger: Logger) {
+ this.logger = logger;
+ }
+
+ [Symbol.iterator]() {
+ return this.definitions.entries();
+ }
+
+ public has(type: string) {
+ return this.definitions.has(type);
+ }
+
+ public get(type: string): TaskDefinition {
+ this.ensureHas(type);
+ return this.definitions.get(type)!;
+ }
+
+ public ensureHas(type: string) {
+ if (!this.has(type)) {
+ throw new Error(
+ `Unsupported task type "${type}". Supported types are ${[...this.definitions.keys()].join(
+ ', '
+ )}`
+ );
+ }
+ }
+
+ /**
+ * Method for allowing consumers to register task definitions into the system.
+ * @param taskDefinitions - The Kibana task definitions dictionary
+ */
+ public registerTaskDefinitions(taskDefinitions: Record>) {
+ const duplicate = Object.keys(taskDefinitions).find((type) => this.definitions.has(type));
+ if (duplicate) {
+ throw new Error(`Task ${duplicate} is already defined!`);
+ }
+
+ try {
+ for (const definition of sanitizeTaskDefinitions(taskDefinitions)) {
+ this.definitions.set(definition.type, definition);
+ }
+ } catch (e) {
+ this.logger.error('Could not sanitize task definitions');
+ }
+ }
+}
+
+/**
+ * Sanitizes the system's task definitions. Task definitions have optional properties, and
+ * this ensures they all are given a reasonable default.
+ *
+ * @param taskDefinitions - The Kibana task definitions dictionary
+ */
+export function sanitizeTaskDefinitions(
+ taskDefinitions: Record>
+): TaskDefinition[] {
+ return Object.entries(taskDefinitions).map(([type, rawDefinition]) =>
+ Joi.attempt({ type, ...rawDefinition }, validateTaskDefinition)
+ );
+}
diff --git a/x-pack/plugins/task_manager/server/test_utils/index.ts b/x-pack/plugins/task_manager/server/test_utils/index.ts
index 6f43a60ff42d28..e882e0fd187035 100644
--- a/x-pack/plugins/task_manager/server/test_utils/index.ts
+++ b/x-pack/plugins/task_manager/server/test_utils/index.ts
@@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import { loggingSystemMock } from 'src/core/server/mocks';
/*
* A handful of helper functions for testing the task manager.
@@ -11,18 +12,9 @@
// Caching this here to avoid setTimeout mocking affecting our tests.
const nativeTimeout = setTimeout;
-/**
- * Creates a mock task manager Logger.
- */
export function mockLogger() {
- return {
- info: jest.fn(),
- debug: jest.fn(),
- warn: jest.fn(),
- error: jest.fn(),
- };
+ return loggingSystemMock.createLogger();
}
-
export interface Resolvable {
resolve: () => void;
}
diff --git a/x-pack/plugins/task_manager/server/types.ts b/x-pack/plugins/task_manager/server/types.ts
deleted file mode 100644
index a38730ad7f768a..00000000000000
--- a/x-pack/plugins/task_manager/server/types.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { TaskManager as TaskManagerClass } from './task_manager';
-
-export type TaskManager = PublicMethodsOf;
-
-export interface Logger {
- info(message: string): void;
- debug(message: string): void;
- warn(message: string): void;
- error(message: string): void;
-}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index d4498a626ab9e3..e645ae32abbd18 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -3153,7 +3153,6 @@
"telemetry.provideUsageStatisticsTitle": "使用統計を提供",
"telemetry.readOurUsageDataPrivacyStatementLinkText": "プライバシーポリシー",
"telemetry.securityData": "Endpoint Securityデータ",
- "telemetry.seeExamplesOfWhatWeCollect": "当社が収集する{clusterData}および{endpointSecurityData}の例をご覧ください。",
"telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。",
"telemetry.telemetryConfigAndLinkDescription": "使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。",
"telemetry.telemetryConfigDescription": "基本的な機能の利用状況に関する統計情報を提供して、Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。",
@@ -11359,7 +11358,6 @@
"xpack.maps.layerWizardSelect.solutionsCategoryLabel": "ソリューション",
"xpack.maps.loadMap.errorAttemptingToLoadSavedMap": "マップを読み込めません",
"xpack.maps.map.initializeErrorTitle": "マップを初期化できません",
- "xpack.maps.mapController.mapsBreadcrumbLabel": "マップ",
"xpack.maps.mapEmbeddableFactory.invalidLayerList": "不正な形式のレイヤーリストによりマップを読み込めません",
"xpack.maps.mapEmbeddableFactory.invalidSavedObject": "不正な形式の保存済みオブジェクトによりマップを読み込めません",
"xpack.maps.mapListing.advancedSettingsLinkText": "高度な設定",
@@ -17346,9 +17344,7 @@
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "クエリプレビュー",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "ノイズ警告:このルールではノイズが多く生じる可能性があります。クエリを絞り込むことを検討してください。これは1時間ごとに1アラートという線形進行に基づいています。",
"xpack.securitySolution.detectionEngine.queryPreview.queryNoHits": "ヒットが見つかりませんでした。",
- "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphCapHitWarning": "クエリの上限サイズ{cap}に達しました。このクエリは表示されている{cap}を超えるヒットを生成できませんでした。",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphThresholdWithFieldTitle": "{buckets} {buckets, plural, =1 {固有のヒット} other {固有のヒット}}",
- "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTimestampWarning": "イベントで「@timestamp」フィールドが見つかりません",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTitle": "{hits} {hits, plural, =1 {ヒット} other {ヒット}}",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "クエリ結果をプレビューするデータのタイムフレームを選択します",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "クイッククエリプレビュー",
@@ -18014,17 +18010,6 @@
"xpack.securitySolution.lists.valueListsUploadError": "値リストのアップロードエラーが発生しました。",
"xpack.securitySolution.lists.valueListsUploadSuccess": "値リスト「{fileName}」はアップロードされませんでした",
"xpack.securitySolution.lists.valueListsUploadSuccessTitle": "値リストがアップロードされました",
- "xpack.securitySolution.markdown.hint.boldLabel": "**太字**",
- "xpack.securitySolution.markdown.hint.bulletLabel": "* ビュレット",
- "xpack.securitySolution.markdown.hint.codeLabel": "`code`",
- "xpack.securitySolution.markdown.hint.headingLabel": "# 見出し",
- "xpack.securitySolution.markdown.hint.imageUrlLabel": "![image](url)",
- "xpack.securitySolution.markdown.hint.italicsLabel": "_italics_",
- "xpack.securitySolution.markdown.hint.preformattedLabel": "```preformatted```",
- "xpack.securitySolution.markdown.hint.quoteLabel": ">引用",
- "xpack.securitySolution.markdown.hint.strikethroughLabel": "取り消し線",
- "xpack.securitySolution.markdown.hint.urlLabel": "[link](url)",
- "xpack.securitySolution.markdown.toolTip.timelineId": "タイムラインID:{ timelineId }",
"xpack.securitySolution.markdownEditor.markdown": "マークダウン",
"xpack.securitySolution.markdownEditor.markdownInputHelp": "Markdown 構文ヘルプ",
"xpack.securitySolution.markdownEditor.plugins.timeline.insertTimelineButtonLabel": "タイムラインリンクの挿入",
@@ -18343,7 +18328,6 @@
"xpack.securitySolution.security.title": "セキュリティ",
"xpack.securitySolution.source.destination.packetsLabel": "パケット",
"xpack.securitySolution.stepDefineRule.previewQueryAriaLabel": "クエリプレビュータイムフレーム選択",
- "xpack.securitySolution.stepDefineRule.previewQueryLabel": "結果を表示",
"xpack.securitySolution.system.acceptedAConnectionViaDescription": "次の手段で接続を受け付けました。",
"xpack.securitySolution.system.acceptedDescription": "以下を経由してユーザーを受け入れました。",
"xpack.securitySolution.system.attemptedLoginDescription": "以下を経由してログインを試行しました:",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 5f1c72929b2c6f..2f701d0cde284b 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -3154,7 +3154,6 @@
"telemetry.provideUsageStatisticsTitle": "提供使用情况统计",
"telemetry.readOurUsageDataPrivacyStatementLinkText": "隐私声明",
"telemetry.securityData": "Endpoint Security 数据",
- "telemetry.seeExamplesOfWhatWeCollect": "查看我们收集的{clusterData}和 {endpointSecurityData}的示例。",
"telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack?数据使用情况收集当前已禁用。启用数据使用情况收集可帮助我们管理并改善产品和服务。有关详情,请参阅我们的{privacyStatementLink}。",
"telemetry.telemetryConfigAndLinkDescription": "启用使用情况数据收集可帮助我们管理并改善产品和服务。有关更多详情,请参阅我们的{privacyStatementLink}。",
"telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。",
@@ -11372,7 +11371,6 @@
"xpack.maps.layerWizardSelect.solutionsCategoryLabel": "解决方案",
"xpack.maps.loadMap.errorAttemptingToLoadSavedMap": "无法加载地图",
"xpack.maps.map.initializeErrorTitle": "无法初始化地图",
- "xpack.maps.mapController.mapsBreadcrumbLabel": "Maps",
"xpack.maps.mapEmbeddableFactory.invalidLayerList": "无法加载地图,图层列表格式不正确",
"xpack.maps.mapEmbeddableFactory.invalidSavedObject": "无法加载地图,已保存对象格式错误",
"xpack.maps.mapListing.advancedSettingsLinkText": "高级设置",
@@ -17364,9 +17362,7 @@
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "查询预览",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "噪音警告:此规则可能会导致大量噪音。考虑缩小您的查询范围。这基于每小时 1 条告警的线性级数。",
"xpack.securitySolution.detectionEngine.queryPreview.queryNoHits": "找不到任何命中。",
- "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphCapHitWarning": "命中查询上限大小为 {cap}。此查询生成的命中数可能大于显示的 {cap}。",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphThresholdWithFieldTitle": "{buckets} 个{buckets, plural, =1 {唯一命中} other {唯一命中}}",
- "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTimestampWarning": "在事件中找不到“@timestamp”字段。",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTitle": "{hits} 个{hits, plural, =1 {命中} other {命中}}",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "选择数据的时间范围以预览查询结果",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "快速查询预览",
@@ -18033,17 +18029,6 @@
"xpack.securitySolution.lists.valueListsUploadError": "上传值列表时出错。",
"xpack.securitySolution.lists.valueListsUploadSuccess": "值列表“{fileName}”已上传",
"xpack.securitySolution.lists.valueListsUploadSuccessTitle": "值列表已上传",
- "xpack.securitySolution.markdown.hint.boldLabel": "**粗体**",
- "xpack.securitySolution.markdown.hint.bulletLabel": "* 项目符号",
- "xpack.securitySolution.markdown.hint.codeLabel": "`code`",
- "xpack.securitySolution.markdown.hint.headingLabel": "# 标题",
- "xpack.securitySolution.markdown.hint.imageUrlLabel": "![图](url)",
- "xpack.securitySolution.markdown.hint.italicsLabel": "_斜体_",
- "xpack.securitySolution.markdown.hint.preformattedLabel": "```预设格式```",
- "xpack.securitySolution.markdown.hint.quoteLabel": ">引文",
- "xpack.securitySolution.markdown.hint.strikethroughLabel": "删除线",
- "xpack.securitySolution.markdown.hint.urlLabel": "[链接](url)",
- "xpack.securitySolution.markdown.toolTip.timelineId": "时间线 id:{ timelineId }",
"xpack.securitySolution.markdownEditor.markdown": "Markdown",
"xpack.securitySolution.markdownEditor.markdownInputHelp": "Markdown 语法帮助",
"xpack.securitySolution.markdownEditor.plugins.timeline.insertTimelineButtonLabel": "插入时间线链接",
@@ -18362,7 +18347,6 @@
"xpack.securitySolution.security.title": "安全",
"xpack.securitySolution.source.destination.packetsLabel": "pkts",
"xpack.securitySolution.stepDefineRule.previewQueryAriaLabel": "查询预览时间范围选择",
- "xpack.securitySolution.stepDefineRule.previewQueryLabel": "预览结果",
"xpack.securitySolution.system.acceptedAConnectionViaDescription": "已接受连接 - 通过",
"xpack.securitySolution.system.acceptedDescription": "已接受该用户 - 通过",
"xpack.securitySolution.system.attemptedLoginDescription": "已尝试登录 - 通过",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx
index 84726bc950ef25..7be7e60c2e19ce 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx
@@ -134,7 +134,7 @@ export const AlertAdd = ({
const newAlert = await createAlert({ http, alert });
toastNotifications.addSuccess(
i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText', {
- defaultMessage: "Saved '{alertName}'",
+ defaultMessage: 'Created alert "{alertName}"',
values: {
alertName: newAlert.name,
},
diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts
index 730bb2277227eb..1100fbf5a828da 100644
--- a/x-pack/plugins/uptime/server/kibana.index.ts
+++ b/x-pack/plugins/uptime/server/kibana.index.ts
@@ -33,8 +33,6 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
name: PLUGIN.NAME,
order: 1000,
category: DEFAULT_APP_CATEGORIES.observability,
- navLinkId: PLUGIN.ID,
- icon: 'uptimeApp',
app: ['uptime', 'kibana'],
catalogue: ['uptime'],
management: {
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts
index 23adf5f8cb9a26..d43c3363f86b1a 100644
--- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts
@@ -76,7 +76,7 @@ function getAlwaysFiringAlertType() {
instanceContextValue: true,
});
}
- await services.callCluster('index', {
+ await services.scopedClusterClient.index({
index: params.index,
refresh: 'wait_for',
body: {
diff --git a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts
index 8f78cdf015601d..e4e6adca9640f8 100644
--- a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts
+++ b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts
@@ -35,7 +35,13 @@ export default ({ getService }: FtrProviderContext) => {
groups: [],
analysis_config: {
bucket_span: '15m',
- detectors: [{ function: 'mean', field_name: 'products.discount_amount' }],
+ detectors: [
+ {
+ function: 'mean',
+ field_name: 'products.discount_amount',
+ exclude_frequent: 'none',
+ },
+ ],
influencers: [],
summary_count_field_name: 'doc_count',
},
diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts
index 78dca5100dece9..ada67bbec070b5 100644
--- a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts
+++ b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts
@@ -24,7 +24,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const fieldNames =
'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name';
- describe('Slow durations', () => {
+ // Failing: See https://github.com/elastic/kibana/issues/81264
+ describe.skip('Slow durations', () => {
const url = format({
pathname: `/api/apm/correlations/slow_durations`,
query: { start, end, durationPercentile, fieldNames },
diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts
index 8ef42052ba9514..86309c91b0bc21 100644
--- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts
+++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts
@@ -31,7 +31,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(response.body.noHits).to.be(true);
- expect(response.body.erroneousTransactionsRate.length).to.be(0);
+ expect(response.body.transactionErrorRate.length).to.be(0);
expect(response.body.average).to.be(null);
});
});
@@ -41,7 +41,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('returns the transaction error rate', () => {
let errorRateResponse: {
- erroneousTransactionsRate: Array<{ x: number; y: number | null }>;
+ transactionErrorRate: Array<{ x: number; y: number | null }>;
average: number;
};
before(async () => {
@@ -54,9 +54,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns some data', () => {
expect(errorRateResponse.average).to.be.greaterThan(0);
- expect(errorRateResponse.erroneousTransactionsRate.length).to.be.greaterThan(0);
+ expect(errorRateResponse.transactionErrorRate.length).to.be.greaterThan(0);
- const nonNullDataPoints = errorRateResponse.erroneousTransactionsRate.filter(
+ const nonNullDataPoints = errorRateResponse.transactionErrorRate.filter(
({ y }) => y !== null
);
@@ -65,26 +65,26 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('has the correct start date', () => {
expectSnapshot(
- new Date(first(errorRateResponse.erroneousTransactionsRate)?.x ?? NaN).toISOString()
+ new Date(first(errorRateResponse.transactionErrorRate)?.x ?? NaN).toISOString()
).toMatchInline(`"2020-09-29T14:30:00.000Z"`);
});
it('has the correct end date', () => {
expectSnapshot(
- new Date(last(errorRateResponse.erroneousTransactionsRate)?.x ?? NaN).toISOString()
+ new Date(last(errorRateResponse.transactionErrorRate)?.x ?? NaN).toISOString()
).toMatchInline(`"2020-09-29T15:00:00.000Z"`);
});
it('has the correct number of buckets', () => {
- expectSnapshot(errorRateResponse.erroneousTransactionsRate.length).toMatchInline(`61`);
+ expectSnapshot(errorRateResponse.transactionErrorRate.length).toMatchInline(`61`);
});
it('has the correct calculation for average', () => {
- expectSnapshot(errorRateResponse.average).toMatchInline(`0.200076804915515`);
+ expectSnapshot(errorRateResponse.average).toMatchInline(`0.152173913043478`);
});
it('has the correct error rate', () => {
- expectSnapshot(errorRateResponse.erroneousTransactionsRate).toMatch();
+ expectSnapshot(errorRateResponse.transactionErrorRate).toMatch();
});
});
});
diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts
index 992685c9696399..a84d9845085e0a 100644
--- a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts
+++ b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts
@@ -79,7 +79,7 @@ export default ({ getService }: FtrProviderContext) => {
it('should be able to execute and get 10 signals', async () => {
const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' };
await createRule(supertest, rule);
- await waitForSignalsToBePresent(supertest);
+ await waitForSignalsToBePresent(supertest, 10);
const signalsOpen = await getAllSignals(supertest);
expect(signalsOpen.hits.hits.length).equal(10);
});
@@ -102,7 +102,7 @@ export default ({ getService }: FtrProviderContext) => {
it('should be able to get a count of 10 closed signals when closing 10', async () => {
const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' };
await createRule(supertest, rule);
- await waitForSignalsToBePresent(supertest);
+ await waitForSignalsToBePresent(supertest, 10);
const signalsOpen = await getAllSignals(supertest);
const signalIds = signalsOpen.hits.hits.map((signal) => signal._id);
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts
index 3cb2356e9e1b5e..ae4ff41c509eae 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts
@@ -435,7 +435,7 @@ export default ({ getService }: FtrProviderContext) => {
],
};
await createRule(supertest, ruleWithException);
- await waitForSignalsToBePresent(supertest);
+ await waitForSignalsToBePresent(supertest, 10);
const signalsOpen = await getAllSignals(supertest);
expect(signalsOpen.hits.hits.length).equal(10);
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts
index 0c425f46232b0c..498c6071217602 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts
@@ -120,7 +120,7 @@ export default ({ getService }: FtrProviderContext) => {
};
await createRule(supertest, rule);
- await waitForSignalsToBePresent(supertest);
+ await waitForSignalsToBePresent(supertest, 10);
const signalsOpen = await getAllSignals(supertest);
expect(signalsOpen.hits.hits.length).equal(10);
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts
index 6e08ee1dbe2ce4..d2a3e86526db4a 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts
@@ -78,7 +78,7 @@ export default ({ getService }: FtrProviderContext) => {
it('should be able to execute and get 10 signals', async () => {
const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' };
await createRule(supertest, rule);
- await waitForSignalsToBePresent(supertest);
+ await waitForSignalsToBePresent(supertest, 10);
const signalsOpen = await getAllSignals(supertest);
expect(signalsOpen.hits.hits.length).equal(10);
});
@@ -101,7 +101,7 @@ export default ({ getService }: FtrProviderContext) => {
it('should be able to get a count of 10 closed signals when closing 10', async () => {
const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' };
await createRule(supertest, rule);
- await waitForSignalsToBePresent(supertest);
+ await waitForSignalsToBePresent(supertest, 10);
const signalsOpen = await getAllSignals(supertest);
const signalIds = signalsOpen.hits.hits.map((signal) => signal._id);
@@ -124,7 +124,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(signalsClosed.hits.hits.length).to.equal(10);
});
- it('should be able close 10 signals immediately and they all should be closed', async () => {
+ it('should be able close signals immediately and they all should be closed', async () => {
const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' };
await createRule(supertest, rule);
await waitForSignalsToBePresent(supertest);
diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts
index 94fbd728be3126..05a0f73dd0dc4f 100644
--- a/x-pack/test/detection_engine_api_integration/utils.ts
+++ b/x-pack/test/detection_engine_api_integration/utils.ts
@@ -794,11 +794,14 @@ export const waitForRuleSuccess = async (
};
/**
- * Waits for the signal hits to be greater than zero before continuing
+ * Waits for the signal hits to be greater than the supplied number
+ * before continuing with a default of at least one signal
* @param supertest Deps
+ * @param numberOfSignals The number of signals to wait for, default is 1
*/
export const waitForSignalsToBePresent = async (
- supertest: SuperTest
+ supertest: SuperTest,
+ numberOfSignals = 1
): Promise => {
await waitFor(async () => {
const {
@@ -808,7 +811,7 @@ export const waitForSignalsToBePresent = async (
.set('kbn-xsrf', 'true')
.send(getQueryAllSignals())
.expect(200);
- return signalsOpen.hits.hits.length > 0;
+ return signalsOpen.hits.hits.length >= numberOfSignals;
});
};
diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js
index a58f66214e772b..c304b177cb04b5 100644
--- a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js
+++ b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js
@@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }) {
const nodesList = getService('monitoringElasticsearchNodes');
const nodeDetail = getService('monitoringElasticsearchNodeDetail');
- describe('Elasticsearch node detail', () => {
+ // Failing: See https://github.com/elastic/kibana/issues/81166
+ describe.skip('Elasticsearch node detail', () => {
describe('Active Nodes', () => {
const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects);
diff --git a/x-pack/test/functional/es_archives/endpoint/resolver_tree/library_events/data.json.gz b/x-pack/test/functional/es_archives/endpoint/resolver_tree/library_events/data.json.gz
new file mode 100644
index 00000000000000..0027df16e689c1
Binary files /dev/null and b/x-pack/test/functional/es_archives/endpoint/resolver_tree/library_events/data.json.gz differ
diff --git a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js
index 55c34615373a93..0cf20b6599acb3 100644
--- a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js
+++ b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js
@@ -86,6 +86,7 @@ export function MonitoringElasticsearchNodesProvider({ getService, getPageObject
}
async clickDiskCol() {
await find.clickByCssSelector(`[data-test-subj="${SUBJ_TABLE_SORT_DISK_COL}"] > button`);
+ await this.waitForTableToFinishLoading();
}
async clickShardsCol() {
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts
index 4a9ead9b020060..ce821b56d3a8ae 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts
@@ -110,7 +110,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.click('saveAlertButton');
const toastTitle = await pageObjects.common.closeToast();
- expect(toastTitle).to.eql(`Saved '${alertName}'`);
+ expect(toastTitle).to.eql(`Created alert "${alertName}"`);
await pageObjects.triggersActionsUI.searchAlerts(alertName);
const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList();
expect(searchResultsAfterSave).to.eql([
@@ -143,7 +143,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.missingOrFail('confirmAlertSaveModal');
const toastTitle = await pageObjects.common.closeToast();
- expect(toastTitle).to.eql(`Saved '${alertName}'`);
+ expect(toastTitle).to.eql(`Created alert "${alertName}"`);
await pageObjects.triggersActionsUI.searchAlerts(alertName);
const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList();
expect(searchResultsAfterSave).to.eql([
diff --git a/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts b/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts
index 2c5d154ab04161..4bd7b81b02d57b 100644
--- a/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts
+++ b/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts
@@ -81,6 +81,24 @@ export default function (providerContext: FtrProviderContext) {
});
createdAgentPolicyIds.push(testPolicyRes.item.id);
+ const beforeRes = await esClient.search({
+ index: '.kibana',
+ body: {
+ query: {
+ bool: {
+ must: [
+ {
+ terms: {
+ type: ['fleet-agent-actions'],
+ },
+ },
+ { match: { 'fleet-agent-actions.policy_id': testPolicyRes.item.id } },
+ ],
+ },
+ },
+ },
+ });
+
await supertest
.put(`/api/fleet/settings`)
.set('kbn-xsrf', 'xxxx')
@@ -105,7 +123,7 @@ export default function (providerContext: FtrProviderContext) {
},
});
- expect(res.hits.hits.length).equal(2);
+ expect(res.hits.hits.length).equal(beforeRes.hits.hits.length + 1);
});
});
}
diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts
index 3ea669ae9d404f..803df6a66ea580 100644
--- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts
+++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts
@@ -92,13 +92,11 @@ export class SampleTaskManagerFixturePlugin
taskManager.registerTaskDefinitions({
sampleTask: {
...defaultSampleTaskConfig,
- type: 'sampleTask',
title: 'Sample Task',
description: 'A sample task for testing the task_manager.',
},
singleAttemptSampleTask: {
...defaultSampleTaskConfig,
- type: 'singleAttemptSampleTask',
title: 'Failing Sample Task',
description:
'A sample task for testing the task_manager that fails on the first attempt to run.',
diff --git a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/plugin.ts b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/plugin.ts
index ba6d7ced3c591a..18449ef61d1acd 100644
--- a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/plugin.ts
+++ b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/plugin.ts
@@ -79,7 +79,6 @@ export class SampleTaskManagerFixturePlugin
taskManager.registerTaskDefinitions({
performanceTestTask: {
- type: 'performanceTestTask',
title,
description: 'A task for stress testing task_manager.',
timeout: '1m',
diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
index e3cb7cd294f464..1f6973ae0f9887 100644
--- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
+++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
@@ -206,6 +206,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
events: { file: false, network: true, process: true },
logging: { file: 'info' },
malware: { mode: 'prevent' },
+ popup: {
+ malware: {
+ enabled: true,
+ message: '',
+ },
+ },
},
windows: {
events: {
@@ -219,6 +225,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
},
logging: { file: 'info' },
malware: { mode: 'prevent' },
+ popup: {
+ malware: {
+ enabled: true,
+ message: '',
+ },
+ },
},
},
streams: [],
diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts
index 91d15f3a272fa7..1af9ec88df8522 100644
--- a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts
+++ b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts
@@ -194,7 +194,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await (await testSubjects.find('resolver:graph-controls:zoom-in')).click();
});
- it('Check Child events for event.file Node', async () => {
+ it('Check Related Events for event.file Node', async () => {
const expectedData = [
'17 authentication',
'1 registry',
@@ -225,7 +225,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await pageObjects.hosts.deleteDataStreams();
});
- it('Check Child events for event.process Node', async () => {
+ it('Check Related Events for event.process Node', async () => {
await pageObjects.hosts.navigateToEventsPanel();
await pageObjects.hosts.executeQueryAndOpenResolver(
'event.dataset : endpoint.events.process'
@@ -233,7 +233,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await pageObjects.hosts.runNodeEvents(expectedData);
});
- it('Check Child events for event.security Node', async () => {
+ it('Check Related Events for event.security Node', async () => {
await pageObjects.hosts.navigateToEventsPanel();
await pageObjects.hosts.executeQueryAndOpenResolver(
'event.dataset : endpoint.events.security'
@@ -241,7 +241,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await pageObjects.hosts.runNodeEvents(expectedData);
});
- it('Check Child events for event.registry Node', async () => {
+ it('Check Related Events for event.registry Node', async () => {
await pageObjects.hosts.navigateToEventsPanel();
await pageObjects.hosts.executeQueryAndOpenResolver(
'event.dataset : endpoint.events.registry'
@@ -249,13 +249,26 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await pageObjects.hosts.runNodeEvents(expectedData);
});
- it('Check Child events for event.network Node', async () => {
+ it('Check Related Events for event.network Node', async () => {
await pageObjects.hosts.navigateToEventsPanel();
await pageObjects.hosts.executeQueryAndOpenResolver(
'event.dataset : endpoint.events.network'
);
await pageObjects.hosts.runNodeEvents(expectedData);
});
+
+ it('Check Related Events for event.library Node', async () => {
+ await esArchiver.load('empty_kibana');
+ await esArchiver.load('endpoint/resolver_tree/library_events', { useCreate: true });
+ await queryBar.setQuery('');
+ await queryBar.submitQuery();
+ const expectedLibraryData = ['329 network', '1 library', '1 library'];
+ await pageObjects.hosts.navigateToEventsPanel();
+ await pageObjects.hosts.executeQueryAndOpenResolver(
+ 'event.dataset : endpoint.events.library'
+ );
+ await pageObjects.hosts.runNodeEvents(expectedLibraryData);
+ });
});
});
}
diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json
index 8e378ff1f4a6af..6f7d693baac7f6 100644
--- a/x-pack/test/tsconfig.json
+++ b/x-pack/test/tsconfig.json
@@ -22,5 +22,8 @@
{ "path": "../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../plugins/licensing/tsconfig.json" },
{ "path": "../plugins/global_search/tsconfig.json" },
+ { "path": "../../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "../../src/plugins/telemetry_collection_manager/tsconfig.json" },
+ { "path": "../../src/plugins/telemetry/tsconfig.json" }
]
}
diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts
index c5d188c4139bf9..25bb6e78845789 100644
--- a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts
+++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts
@@ -17,9 +17,7 @@ class FooPlugin implements Plugin {
plugins.features.registerKibanaFeature({
id: 'foo',
name: 'Foo',
- icon: 'upArrow',
category: { id: 'foo', label: 'foo' },
- navLinkId: 'foo_plugin',
app: ['foo_plugin', 'kibana'],
catalogue: ['foo'],
privileges: {
diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json
index f751aac1806dd9..1d392890b27fdc 100644
--- a/x-pack/tsconfig.json
+++ b/x-pack/tsconfig.json
@@ -14,6 +14,9 @@
"plugins/apm/scripts/**/*",
"plugins/licensing/**/*",
"plugins/global_search/**/*",
+ "../src/plugins/usage_collection/**/*",
+ "../src/plugins/telemetry_collection_manager/**/*",
+ "../src/plugins/telemetry/**/*"
],
"compilerOptions": {
"paths": {
@@ -31,5 +34,8 @@
{ "path": "../src/plugins/kibana_react/tsconfig.json" },
{ "path": "./plugins/licensing/tsconfig.json" },
{ "path": "./plugins/global_search/tsconfig.json" },
+ { "path": "../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" },
+ { "path": "../src/plugins/telemetry/tsconfig.json" }
]
}