From 1e067b1608adc89cf647f47a24bcecde2d3e99fe Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 30 Jul 2020 14:01:29 -0400 Subject: [PATCH 01/55] [Security Solution][Resolver] Adding resolver backend docs (#73726) * Adding resolver backend docs * Adding more clarity around ancestry array limit Co-authored-by: Elastic Machine --- .../common/endpoint/types.ts | 2 + .../endpoint/routes/resolver/docs/README.md | 216 ++++++++++++++++++ .../resolver/docs/resolver_tree_ancestry.png | Bin 0 -> 19926 bytes .../docs/resolver_tree_children_loop.png | Bin 0 -> 40224 bytes .../resolver_tree_children_pagination.png | Bin 0 -> 32477 bytes ...er_tree_children_pagination_with_after.png | Bin 0 -> 32398 bytes .../docs/resolver_tree_children_simple.png | Bin 0 -> 22889 bytes .../resolver/utils/ancestry_query_handler.ts | 24 +- .../endpoint/routes/resolver/utils/tree.ts | 33 +-- 9 files changed, 242 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_loop.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination_with_after.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index a982f9ffe8f21..1c24e1abe5a57 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -104,6 +104,8 @@ export interface ResolverChildNode extends ResolverLifecycleNode { * * string: Indicates this is a leaf node and it can be used to continue querying for additional descendants * using this node's entity_id + * + * For more information see the resolver docs on pagination [here](../../server/endpoint/routes/resolver/docs/README.md#L129) */ nextChild?: string | null; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md new file mode 100644 index 0000000000000..1c0692db344c4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md @@ -0,0 +1,216 @@ +# Resolver Backend + +This readme will describe the backend implementation for resolver. + +## Ancestry Array + +The ancestry array is an array of entity_ids. This array is included with each event sent by the elastic endpoint +and defines the ancestors of a particular process. The array is formatted such that [0] of the array contains the direct +parent of the process. [1] of the array contains the grandparent of the process. For example if Process A spawned process +B which spawned process C. Process C's array would be [B,A]. + +The presence of the ancestry array makes querying ancestors and children for a process more efficient. + +## Ancestry Array Limit + +The ancestry array is currently limited to 20 values. The exact limit should not be relied on though. + +## Ancestors + +To query for ancestors of a process leveraging the ancestry array, we first retrieve the lifecycle events for the event_id +passed in. Once we have the origin node we can check to see if the document has the `process.Ext.ancestry` array. If +it does we can perform a search for the values in the array. This will retrieve all the ancestors for the process of interest +up to the limit of the ancestry array. Since the array is capped at 20, if the request is asking for more than 20 +ancestors we will have to examine the most distant ancestor that has been retrieved and use its ancestry array to retrieve +the next set of results to fulfill the request. + +### Pagination + +After the backend gathers the results for an ancestry query, it will set a pagination cursor depending on the results from ES. + +If the number of ancestors we have gathered is equal to the size in the request we don't know if ES has more results or not. So we will set `nextAncestor` to the entity_id of the most distant ancestor retrieved. + +If the request asked for 10 and we only found 8 from ES, we know for sure that there aren't anymore results. In this case we will set `nextAncestor` to `null`. + +### Code + +The code for handling the ancestor logic is in [here](../utils/ancestry_query_handler.ts) + +### Ancestors Multiple Queries Example + +![alt text](./resolver_tree_ancestry.png 'Retrieve ancestors') + +For this example let's assume that the _ancestry array limit_ is 2. The process of interest is A (the entity_id of a node is the character in the circle). Process A has an ancestry array of `[3,2]`, its parent has an ancestry array of `[2,1]` etc. Here is the execution of a request for 3 ancestors for entity_id A. + +**Request:** `GET /resolver/A/ancestry?ancestors=3` + +1. Retrieve lifecycle events for entity_id `A` +2. Retrieve `A`'s start event's ancestry array + 1. In the event that the node of interest does not have an ancestry array, the entity id of it's parent will be used, essentially an ancestry array of length 1, [3] in the example here +3. `A`'s ancestry array is `[3,2]`, query for the lifecycle events for processes with `entity_id` 3 or 2 +4. Check to see if we have retrieved enough ancestors to fulfill the request (we have not, we only received 2 nodes of the 3 that were requested) +5. We haven't so use the most distant ancestor in our result set (process 2) +6. Use process 2's ancestry array to query for the next set of results to fulfill the request +7. Process 2's ancestry array is `[1]` so repeat the process in steps 3-4 and retrieve process with entity_id 1. This fulfills the request so we can return the results for the lifecycle events of A, 3, 2, and 1. + +If process 2 had an ancestry array of `[1,0]` we know that we only need 1 more process to fulfill the request so we can truncate the array to `[1]` instead of searching for all the entries in the array. + +More generically: In the event where our request stops at the x (non-final) position in an ancestry array, we won't search all items in the array, just those up to the x position. The next-cursor will be set to the last ancestor received since there might be more data. + +The `nextAncestor` cursor will be set to `1` in this scenario because we retrieved all 3 ancestors from ES but we don't know if ES has anymore. + +## Descendants + +We can also leverage the ancestry array to query for the descendants of a process. The basic query for the descendants of a process is: _find all processes where their ancestry array contains a particular entity_id_. The results of this query will be sorted in ascending order by the timestamp field. I will try to outline a couple different scenarios for retrieving descendants using the ancestry array below. + +### Start events vs all lifecycle events + +There are two parts to querying for descendant process nodes. When a request comes in for 7 process nodes we need to communicate to ES that we want all of the lifecycle nodes for 7 processes. We could use a query that retrieves all lifecycle events (start, end, etc) but the issue with this is that we need to indicate a `size` in our ES query. If we set the `size` to 7, we will only get 7 lifecycle events. These events could be start, end, or already_running events. It doesn't guarantee that we get all of the lifecycle events for 7 process nodes. + +Instead we can first query for 7 start events, which guarantees that we will have 7 unique process descendants and then we can gather all those entity_ids and do another query for all the lifecycle events for those 7 processes. The downside here is that you have to do two queries to retrieve all the lifecycle events. Optimizations can be made for the first query for the start events by reducing the `_source` that ES returns to only include the `entity_id` and `ancestry`. This will reduce the amount of data that ES has to send back and speed up the query. + +### Scenario Background + +In the scenarios below let's assume the _ancestry array limit_ is 2. The times next to the nodes are the time the node was spawned. The value in red indicates that the process terminated at the time in red. + +Let's also ignore the fact that retrieving the lifecycle events for a descendant actually takes two queries. Let's assume that it's taken care of, and when we say "query for lifecycle events" we get all the lifecycle events back for the descendants using the algorithm described in the [previous section](#start-events-vs-all-lifecycle-events) + +### Simple Scenario + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> E -> F -> G -> H. + +![alt text](./resolver_tree_children_simple.png 'Descendants Simple Scenario') + +**Request:** `GET /resolver/A/children?children=6` + +For this scenario we will retrieve all the lifecycle events for 6 descendants of the process with entity_id `A`. As shown in the diagram above ES has 6 descendants for A so the response to this request will be: `[B, C, E, F, G, H]` because the results are sorted in ascending ordering based on the `timestamp` field which is when the process was started. + +### Looping Scenario + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> E -> F -> G -> J -> K -> H. + +![alt text](./resolver_tree_children_loop.png 'Descendants Looping Scenario') + +**Request:** `GET /resolver/A/children?children=9` + +In this scenario the request is for more descendants than can be retrieved using a single querying with the entity_id `A`. This is because the ancestry array for the descendants in the red section do not have `A` in their ancestry array. So when we query for all process nodes that have `A` in their ancestry array we won't receive D, J, or K. + +Like in the previous scenario, for the first query we will receive `[B, C, E, F, G, H]`. What we want to do next is use a subset of that response that will get us 3 more descendants to fulfill the request for a total of 9. + +We _could_ use `B` and `G` to do this (mostly `B`) but the problem is that when we query for descendants that have `B` or `G` in their ancestry array we will get back some duplicates that we have already received before. For example if we use `B` and `G` we'd get `[C, D, E, F, J, K, H]` but this isn't efficient because we have already received `[E, F, G, H]` from the previous query. + +What we want to do is use the most distant descendants from `A` to make the next query to retrieve the last 3 process nodes to fulfill the request. Those would be `[C, E, F, H]`. So our next query will be: _find all process nodes where their ancestry array contains C or E or F or H_. This query can be limited to a size of 3 so that we will only receive `[D, J, K]`. + +We have now received all the nodes for the request and we can return the results as `[B, C, E, F, G, H, D, J, K]`. + +### Important Caveats + +#### Ordering + +In the previous example the final results are not sorted based on timestamp in ascending order. This is because we had to perform multiple queries to retrieve all the results. The backend will not return the results in sorted order. + +#### Tie breaks on timestamp + +In the previous example we saw that J and K had the same timestamp of `12:13 pm`. The reason they were returned in the order `[J, K]` is because the `event.id` field is used to break ties like this. The `event.id` field is unique for a particular event and an increasing value per ECS's guidelines. Therefore J comes before K because it has an `event.id` of 1 vs 2. + +#### Finding the most distant descendants + +In the previous scenario we saw that we needed to use the most distant descendants from a particular node. To determine if a node is a most distant descendant we can use the ancestry array. Nodes C, E, F, and H all have `A` as their last entry in the ancestry array. This indicates that they are a distant descendant that should be used in the next query. There's one problem with this approach. In a mostly impossible scenario where the node of interest (A) does not have any ancestors, its direct children will also have `A` as the last entry in their ancestry array. + +This edge case will likely never be encountered but we'll try to solve it anyway. To get around this as we iterate over the results from our first query (`[B, C, E, F, G, H]`) we can bucket the ones that have `A` as the last entry in their ancestry array. We bucket the results based on the length of their ancestry array (basically a `Map>`). So after bucketing our results will look like: + +```javascript +{ + 1: [B, G] + 2: [C, E, F, H] +} +``` + +While we are iterating we also keep track of the largest ancestry array that we have seen. In our scenario that will be a size of 2. Then to determine the distant descendants we simply get the nodes that had the largest ancestry array length. In this scenario that'd be `[C, E, F, H]`. + +### Handling Pagination + +#### Pagination Cursor Values + +There are 3 possible states for the pagination cursor for a child node and 2 possible states for the pagination cursor for the node of interest (the node that we are using in the API request). + +Potential cursors for the node of interest: +**a string cursor:** a cursor that can be used to skip the previous set of results. The cursor is a base64 version of a json object with `event.id` and `timestamp` of the last process's start event that was received. +**null:** indicates that no more results can be received using this process's entity_id + +Potential cursors for descendants of the node of interest (these apply to the results of a request): +**a string cursor:** a cursor that can be used to skip the previous set of results. The cursor is a base64 version of a json object with `event.id` and `timestamp` of the last process's start event that was received. This cursor should be used in conjunction with using this process's entity_id for a query. +**undefined:** the node may contain additional children, but we are not aware. To find out, perform additional queries on the node of interest that original returned these results or move down the tree to a descendant of this node to query for more descendants. +**null:** We have found all possible direct children for this node. There may be more descendants but not direct children for this node. + +#### Pagination Examples + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K. + +![alt text](./resolver_tree_children_pagination.png 'Descendants Pagination Scenario') + +Handling pagination for the children API in a little tricky. Let's consider this scenario: + +**Request:** `GET /resolver/A/children?children=3` + +Let's use the diagram above to show the relationship between processes and the data in ES. The response for the request for 3 children is `[B, C, G]`. More process nodes exist in ES so it would be helpful to indicate in the response that there is more data and a way to skip `[B, C, G]` to get the next set of data. A cursor can be set on the response for `A` to point to the last process node in the response which can be sent in another request to retrieve the next set of data. This cursor will contain information from `G` because it has the latest timestamp (in ascending order). + +If another request was made using the returned cursor like the following: + +**Request:** `GET /resolver/A/children?children=5&after=` + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination_with_after.png 'Descendants Pagination Scenario Part 2') + +For this request we will do a query for _find all process nodes where their ancestry array has entity_id `A` and use the cursor to skip old results_. The response for this request is `[F, H, E, K]`. The request actually asked for 5 nodes but there was only 4 in ES so only 4 were returned. + +The odd thing about this response is that it did not receive D and J. The problem is that the backend does not have any concept of C in this second request because it was received in the previous one. It will be skipped based on the pagination cursor returned previously. + +This example highlights a scenario where it is not easy for the backend to go back and continue to get the descendants for C because of the limitation of the ancestry array. + +#### Pagination cursor for descendant nodes + +Let's go back to the first request where we got `[B, C, G]`. How could we go about getting the rest of the children for `B`? We have two ways of solving this. First we could determine what the last descendant we had received of `B` and use that as the cursor when returning all the results for this request. That is actually kind of difficult. + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination.png 'Descendants Pagination Scenario') + +Let's imagine for a moment that the _ancestry array limit_ is 3 instead of 2. Taking our previous request we would instead get `[B, C, D]` because D started before G. In this case the last descendant for `B` is actually `D` and not `C`. This gets complicated because we'd have to keep track of which descendant was the last (time wise) one for each intermediate process node. In this example we'd need to find the last descendant for both `B` and `C`. We'd have to track the descendants for each process node and build a map to quickly be able to retrieve the last descendant. + +Instead of doing that we could also continue to get the immediate children of `B` by doing another request for `A` like was shown in previous example when using the after cursor. This would guarantee that all children (first level descendants of a node) had been retrieved. + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination_with_after.png 'Descendants Pagination Scenario Part 2') + +To get to the response shown in the diagram above (the blue nodes is the response) a request was made for 3 nodes which returned `[B, C, D]` and then another request was made using the returned cursor for `A` to get an additional 4 nodes `[F, H, E, K]`. Let's assume that the request looked like: + +**Request:** `GET /resolver/A/children?children=5&after=` + +So 5 nodes were actually asked for. After `[F, H, E]` are returned during the first query to ES, we will use the most distant children (also `[F, H, E]`) and make another request for any nodes that have F or H or E in their ancestry array. Only a single node satisfies that query which returns `K`. Therefore we can know with certainty that E, F, and H have no more children because ES only returned K instead of K and one more node (since we requested a total of 5). With this knowledge we can mark A, E, F, H's pagination cursors in a way to communicate that they have no more descendants. + +The way the backend communicates this is by marking the cursor as null. + +If the request was actually for only 4 children like: + +**Request:** `GET /resolver/A/children?children=4&after=` + +Then we wouldn't know for sure whether ES had more results than K. But what we can know is that `A` does not have any more descendants that we can retrieve in a single query using its ancestry array. We have received all nodes where `A` is in their ancestry array when we made the second query for nodes E, F, and H and received `K`. Therefore at the moment when we received `[F, H, E]` we can mark `A`'s cursor as null. + +When we make the next query for any nodes that have F or H or E in their ancestry array and get back K, this satisfies our size of only needing one more node (4 total). At this point we don't know for sure if E, F, or H have more descendants. Since we don't know we will mark E, F, and H's cursors to point to K. + +#### Undefined Pagination + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> E -> F -> G -> J -> K -> H. (Not the same as above). + +For this scenario let's assume ES has the data in the diagram below. Let's say the request looks like: + +**Request:** `GET /resolver/A/children?children=6` + +![alt text](./resolver_tree_children_loop.png 'Descendants Looping Scenario') + +The result for this request will be `[B, C, E, F, G, H]`. Since the request was looking for 6 nodes and we got that amount the cursor for `A` will be set to the last one: `H`. The cursors for the intermediate nodes `B` and `G` will be undefined. This is because we don't know if `B` or `G` have more children but `A` can be used to determine that. We also don't know if C, E, F, or H have more descendants so their cursor will also be marked as undefined. If we wanted to know if C had more descendants we can simply issue a new request like `GET /resolver/C/children` to get its descendants and we won't receive and duplicates because we never received D, J or K. + +If we want to know if `B` has more children we can issue another request using the cursor set for `A`. diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png new file mode 100644 index 0000000000000000000000000000000000000000..a2636c7cd38cb515849ede721f4e4d24a851241a GIT binary patch literal 19926 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
99kI%2SH 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

1~>f(gVdq^NZINKai89#6pt6sG< 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png new file mode 100644 index 0000000000000000000000000000000000000000..f05963c0f0b677ab0882628de635bff7714a85c1 GIT binary patch literal 22889 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%0Dcg{&6(YS?=)aT0-r6RgHFfT*uf(W7+h$d9Q`N+ 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 { // bucket the start and end events together for a single node const ancestryNodes = this.toMapOfNodes(results); - // the order of this array is going to be weird, it will look like this - // [furthest grandparent...closer grandparent, next recursive call furthest grandparent...closer grandparent] + /** + * This array (this.ancestry.ancestors) is the accumulated ancestors of the node of interest. This array is different + * from the ancestry array of a specific document. The order of this array is going to be weird, it will look like this + * [most distant ancestor...closer ancestor, next recursive call most distant ancestor...closer ancestor] + * + * Here is an example of why this happens + * Consider the following tree: + * A -> B -> C -> D -> E -> Origin + * Where A was spawn before B, which was before C, etc + * + * Let's assume the ancestry array limit is 2 so Origin's array would be: [E, D] + * E's ancestry array would be: [D, C] etc + * + * If a request comes in to retrieve all the ancestors in this tree, the accumulate results will be: + * [D, E, B, C, A] + * + * The first iteration would retrieve D and E in that order because they are sorted in ascending order by timestamp. + * The next iteration would get the ancestors of D (since that's the most distant ancestor from Origin) which are + * [B, C] + * The next iteration would get the ancestors of B which is A + * Hence: [D, E, B, C, A] + */ this.ancestry.ancestors.push(...ancestryNodes.values()); this.ancestry.nextAncestor = parentEntityId(results[0]) || null; this.levels = this.levels - ancestryNodes.size; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts index 9e47f4eb94485..3f941851a4143 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts @@ -30,38 +30,9 @@ export interface Options { } /** - * This class aids in constructing a tree of process events. It works in the following way: + * This class aids in constructing a tree of process events. * - * 1. We construct a tree structure starting with the root node for the event we're requesting. - * 2. We leverage the ability to pass hashes and arrays by reference to construct a fast cache of - * process identifiers that updates the tree structure as we push values into the cache. - * - * When we query a single level of results for child process events we have a flattened, sorted result - * list that we need to add into a constructed tree. We also need to signal in an API response whether - * or not there are more child processes events that we have not yet retrieved, and, if so, for what parent - * process. So, at the end of our tree construction we have a relational layout of the events with no - * pagination information for the given parent nodes. In order to actually construct both the tree and - * insert the pagination information we basically do the following: - * - * 1. Using a terms aggregation query, we return an approximate roll-up of the number of child process - * "creation" events, this gives us an estimation of the number of associated children per parent - * 2. We feed these child process creation event "unique identifiers" (basically a process.entity_id) - * into a second query to get the current state of the process via its "lifecycle" events. - * 3. We construct the tree above with the "lifecycle" events. - * 4. Using the terms query results, we mark each non-leaf node with the number of expected children, if our - * tree has less children than expected, we create a pagination cursor to indicate "we have a truncated set - * of values". - * 5. We mark each leaf node (the last level of the tree we're constructing) with a "null" for the expected - * number of children to indicate "we have not yet attempted to get any children". - * - * Following this scheme, we use exactly 2 queries per level of children that we return--one for the pagination - * and one for the lifecycle events of the processes. The downside to this is that we need to dynamically expand - * the number of documents we can retrieve per level due to the exponential fanout of child processes, - * what this means is that noisy neighbors for a given level may hide other child process events that occur later - * temporally in the same level--so, while a heavily forking process might get shown, maybe the actually malicious - * event doesn't show up in the tree at the beginning. - * - * This Tree's root/origin could be in the middle of the tree. The origin corresponds to the id passed in when this + * This Tree's root/origin will likely be in the middle of the tree. The origin corresponds to the id passed in when this * Tree object is constructed. The tree can have ancestors and children coming from the origin. */ export class Tree { From 827e91c4474e8790374417d30b433296f909b110 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 30 Jul 2020 11:16:49 -0700 Subject: [PATCH 02/55] move and unify postcss config into `@kbn/optimizer` (#73633) Co-authored-by: spalger --- .eslintrc.js | 1 + package.json | 2 +- packages/kbn-optimizer/package.json | 1 + .../{src/worker => }/postcss.config.js | 0 .../basic_optimization.test.ts | 2 +- .../src/worker/webpack.config.ts | 2 +- .../kbn-storybook/lib/webpack.dll.config.js | 2 +- .../storybook_config/webpack.config.js | 2 +- packages/kbn-ui-framework/Gruntfile.js | 2 +- .../doc_site/postcss.config.js | 22 ------------------- packages/kbn-ui-framework/package.json | 4 ++-- packages/kbn-ui-shared-deps/package.json | 1 + src/optimize/base_optimizer.js | 2 +- x-pack/package.json | 7 +++++- .../shareable_runtime/postcss.config.js | 1 - .../shareable_runtime/webpack.config.js | 2 +- .../canvas/storybook/webpack.config.js | 8 +++++-- .../canvas/storybook/webpack.dll.config.js | 2 +- 18 files changed, 26 insertions(+), 37 deletions(-) rename packages/kbn-optimizer/{src/worker => }/postcss.config.js (100%) delete mode 100644 packages/kbn-ui-framework/doc_site/postcss.config.js diff --git a/.eslintrc.js b/.eslintrc.js index c9f9d96f9ddae..b3d29c9866411 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -529,6 +529,7 @@ module.exports = { 'x-pack/test_utils/**/*', 'x-pack/gulpfile.js', 'x-pack/plugins/apm/public/utils/testHelpers.js', + 'x-pack/plugins/canvas/shareable_runtime/postcss.config.js', ], rules: { 'import/no-extraneous-dependencies': [ diff --git a/package.json b/package.json index 51a41cbbab9ff..880534997cff0 100644 --- a/package.json +++ b/package.json @@ -480,7 +480,7 @@ "pixelmatch": "^5.1.0", "pkg-up": "^2.0.0", "pngjs": "^3.4.0", - "postcss": "^7.0.26", + "postcss": "^7.0.32", "postcss-url": "^8.0.0", "prettier": "^2.0.5", "proxyquire": "1.8.0", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index c11bd1b646933..4fbbc920c4447 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -36,6 +36,7 @@ "loader-utils": "^1.2.3", "node-sass": "^4.13.0", "normalize-path": "^3.0.0", + "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "resolve-url-loader": "^3.1.1", diff --git a/packages/kbn-optimizer/src/worker/postcss.config.js b/packages/kbn-optimizer/postcss.config.js similarity index 100% rename from packages/kbn-optimizer/src/worker/postcss.config.js rename to packages/kbn-optimizer/postcss.config.js diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index 2d0d60da1e4a0..bab47d4a1e412 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -161,6 +161,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { Array [ /node_modules/css-loader/package.json, /node_modules/style-loader/package.json, + /packages/kbn-optimizer/postcss.config.js, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, @@ -171,7 +172,6 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7dark.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss, /packages/kbn-optimizer/target/worker/entry_point_creator.js, - /packages/kbn-optimizer/target/worker/postcss.config.js, /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 3d62ed1636869..ae5d2b5fb3292 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -152,7 +152,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: options: { sourceMap: !worker.dist, config: { - path: require.resolve('./postcss.config'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-storybook/lib/webpack.dll.config.js b/packages/kbn-storybook/lib/webpack.dll.config.js index 740ee3819c36f..661312b9a0581 100644 --- a/packages/kbn-storybook/lib/webpack.dll.config.js +++ b/packages/kbn-storybook/lib/webpack.dll.config.js @@ -127,7 +127,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(REPO_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index b2df4f40d4fbe..0a9977463aee8 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -91,7 +91,7 @@ module.exports = async ({ config }) => { loader: 'postcss-loader', options: { config: { - path: resolve(REPO_ROOT, 'src/optimize/'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-ui-framework/Gruntfile.js b/packages/kbn-ui-framework/Gruntfile.js index b7ba1e87b2f00..bb8e7b72cb7bd 100644 --- a/packages/kbn-ui-framework/Gruntfile.js +++ b/packages/kbn-ui-framework/Gruntfile.js @@ -19,7 +19,7 @@ const sass = require('node-sass'); const postcss = require('postcss'); -const postcssConfig = require('../../src/optimize/postcss.config'); +const postcssConfig = require('@kbn/optimizer/postcss.config.js'); const chokidar = require('chokidar'); const { debounce } = require('lodash'); diff --git a/packages/kbn-ui-framework/doc_site/postcss.config.js b/packages/kbn-ui-framework/doc_site/postcss.config.js deleted file mode 100644 index 571bae86dee37..0000000000000 --- a/packages/kbn-ui-framework/doc_site/postcss.config.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -module.exports = { - plugins: [require('autoprefixer')()], -}; diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index abf64906e0253..7933ce06d6847 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -33,7 +33,7 @@ "@babel/core": "^7.10.2", "@elastic/eui": "0.0.55", "@kbn/babel-preset": "1.0.0", - "autoprefixer": "^9.7.4", + "@kbn/optimizer": "1.0.0", "babel-loader": "^8.0.6", "brace": "0.11.1", "chalk": "^2.4.2", @@ -54,7 +54,7 @@ "keymirror": "0.1.1", "moment": "^2.24.0", "node-sass": "^4.13.1", - "postcss": "^7.0.26", + "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "react-dom": "^16.12.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 8398d1c081da6..3c03a52383f77 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -21,6 +21,7 @@ "custom-event-polyfill": "^0.3.0", "elasticsearch-browser": "^16.7.0", "jquery": "^3.5.0", + "mini-css-extract-plugin": "0.8.0", "moment": "^2.24.0", "moment-timezone": "^0.5.27", "react": "^16.12.0", diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 41628a2264193..74973887ae9c1 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -34,7 +34,7 @@ import { IS_KIBANA_DISTRIBUTABLE } from '../legacy/utils'; import { fromRoot } from '../core/server/utils'; import { PUBLIC_PATH_PLACEHOLDER } from './public_path_placeholder'; -const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config'); +const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config.js'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); const EMPTY_MODULE_PATH = require.resolve('./intentionally_empty_module.js'); diff --git a/x-pack/package.json b/x-pack/package.json index 3a9b3ca606de6..2d7cb148c43b0 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -121,8 +121,10 @@ "@types/pretty-ms": "^5.0.0", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", + "autoprefixer": "^9.7.4", "axios": "^0.19.0", "babel-jest": "^25.5.1", + "babel-loader": "^8.0.6", "babel-plugin-require-context-hook": "npm:babel-plugin-require-context-hook-babel7@1.0.0", "base64-js": "^1.3.1", "base64url": "^3.0.1", @@ -159,6 +161,7 @@ "loader-utils": "^1.2.3", "madge": "3.4.4", "marge": "^1.0.1", + "mini-css-extract-plugin": "0.8.0", "mocha": "^7.1.1", "mocha-junit-reporter": "^1.23.1", "mochawesome": "^4.1.0", @@ -168,6 +171,9 @@ "node-fetch": "^2.6.0", "null-loader": "^3.0.0", "pixelmatch": "^5.1.0", + "postcss": "^7.0.32", + "postcss-loader": "^3.0.0", + "postcss-prefix-selector": "^1.7.2", "proxyquire": "1.8.0", "react-docgen-typescript-loader": "^3.1.1", "react-is": "^16.8.0", @@ -308,7 +314,6 @@ "pluralize": "3.1.0", "pngjs": "3.4.0", "polished": "^1.9.2", - "postcss-prefix-selector": "^1.7.2", "prop-types": "^15.6.0", "proper-lockfile": "^3.2.0", "puid": "1.0.7", diff --git a/x-pack/plugins/canvas/shareable_runtime/postcss.config.js b/x-pack/plugins/canvas/shareable_runtime/postcss.config.js index 10baaddfc9b05..e1db6e4a64f71 100644 --- a/x-pack/plugins/canvas/shareable_runtime/postcss.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/postcss.config.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line const autoprefixer = require('autoprefixer'); const prefixer = require('postcss-prefix-selector'); diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 93dc3dbccd549..43e422a161569 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -111,7 +111,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/x-pack/plugins/canvas/storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js index 927f71b832ba0..982185a731b14 100644 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -77,7 +77,9 @@ module.exports = async ({ config }) => { { loader: 'postcss-loader', options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + config: { + path: require.resolve('@kbn/optimizer/postcss.config.js'), + }, }, }, { @@ -114,7 +116,9 @@ module.exports = async ({ config }) => { { loader: 'postcss-loader', options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + config: { + path: require.resolve('@kbn/optimizer/postcss.config.js'), + }, }, }, { diff --git a/x-pack/plugins/canvas/storybook/webpack.dll.config.js b/x-pack/plugins/canvas/storybook/webpack.dll.config.js index 0e9371e4cb5e4..81d19c035075f 100644 --- a/x-pack/plugins/canvas/storybook/webpack.dll.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.dll.config.js @@ -114,7 +114,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, From 70d4eac30c381802d87a5112825699ae6cd8089f Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 30 Jul 2020 14:43:33 -0400 Subject: [PATCH 03/55] [Security Solution] Adding tests for endpoint package pipelines (#73703) * Adding tests for endpoint package pipelines * Removing content type check on types that can change based on docker image version * Skipping ingest tests instead of remove expect * Switching ingest tests over to use application/json * Removing country names Co-authored-by: Elastic Machine --- .../apis/epm/file.ts | 6 +- .../ingest_manager_api_integration/config.ts | 4 +- .../apis/fixtures/package_registry_config.yml | 2 + .../apis/index.ts | 1 + .../apis/package.ts | 140 ++++++++++++++++++ .../apis/resolver/entity_id.ts | 20 +-- .../services/resolver.ts | 25 +++- 7 files changed, 168 insertions(+), 30 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/package.ts diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts index 733b8d4fd9bd6..3f99f91394d2c 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts @@ -47,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) { '/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/visualization/sample_visualization.json' ) .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); @@ -61,7 +61,7 @@ export default function ({ getService }: FtrProviderContext) { '/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/dashboard/sample_dashboard.json' ) .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); @@ -73,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) { await supertest .get('/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/search/sample_search.json') .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); diff --git a/x-pack/test/ingest_manager_api_integration/config.ts b/x-pack/test/ingest_manager_api_integration/config.ts index ddb49a09a7afa..85d1c20c7f155 100644 --- a/x-pack/test/ingest_manager_api_integration/config.ts +++ b/x-pack/test/ingest_manager_api_integration/config.ts @@ -10,9 +10,9 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { defineDockerServersConfig } from '@kbn/test'; // Docker image to use for Ingest Manager API integration tests. -// This hash comes from the commit hash here: https://github.com/elastic/package-storage/commit/48f3935a72b0c5aacc6fec8ef36d559b089a238b +// This hash comes from the commit hash here: https://github.com/elastic/package-storage/commit export const dockerImage = - 'docker.elastic.co/package-registry/distribution:48f3935a72b0c5aacc6fec8ef36d559b089a238b'; + 'docker.elastic.co/package-registry/distribution:80e93ade87f65e18d487b1c407406825915daba8'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml b/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml index 4d93386b4d4e1..00e01fe9ea0fc 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml +++ b/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml @@ -1,2 +1,4 @@ package_paths: - /packages/production + - /packages/staging + - /packages/snapshot diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 56adc2382e234..b1317c2d9f1c1 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -31,5 +31,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./artifacts')); + loadTestFile(require.resolve('./package')); }); } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts new file mode 100644 index 0000000000000..3b5873d1fe0cd --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SearchResponse } from 'elasticsearch'; +import { eventsIndexPattern } from '../../../plugins/security_solution/common/endpoint/constants'; +import { + EndpointDocGenerator, + Event, +} from '../../../plugins/security_solution/common/endpoint/generate_data'; +import { FtrProviderContext } from '../ftr_provider_context'; +import { InsertedEvents, processEventsIndex } from '../services/resolver'; + +interface EventIngested { + event: { + ingested: number; + }; +} + +interface NetworkEvent { + source: { + geo?: { + country_name: string; + }; + }; + destination: { + geo?: { + country_name: string; + }; + }; +} + +const networkIndex = 'logs-endpoint.events.network-default'; + +export default function ({ getService }: FtrProviderContext) { + const resolver = getService('resolverGenerator'); + const es = getService('es'); + const generator = new EndpointDocGenerator('data'); + + const searchForID = async (id: string) => { + return es.search>({ + index: eventsIndexPattern, + body: { + query: { + bool: { + filter: [ + { + ids: { + values: id, + }, + }, + ], + }, + }, + }, + }); + }; + + describe('Endpoint package', () => { + describe('ingested processor', () => { + let event: Event; + let genData: InsertedEvents; + + before(async () => { + event = generator.generateEvent(); + genData = await resolver.insertEvents([event]); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('sets the event.ingested field', async () => { + const resp = await searchForID(genData.eventsInfo[0]._id); + expect(resp.body.hits.hits[0]._source.event.ingested).to.not.be(undefined); + }); + }); + + describe('geoip processor', () => { + let processIndexData: InsertedEvents; + let networkIndexData: InsertedEvents; + + before(async () => { + // 46.239.193.5 should be in Iceland + // 8.8.8.8 should be in the US + const eventWithBothIPs = generator.generateEvent({ + extensions: { source: { ip: '8.8.8.8' }, destination: { ip: '46.239.193.5' } }, + }); + + const eventWithSourceOnly = generator.generateEvent({ + extensions: { source: { ip: '8.8.8.8' } }, + }); + networkIndexData = await resolver.insertEvents( + [eventWithBothIPs, eventWithSourceOnly], + networkIndex + ); + + processIndexData = await resolver.insertEvents([eventWithBothIPs], processEventsIndex); + }); + + after(async () => { + await resolver.deleteData(networkIndexData); + await resolver.deleteData(processIndexData); + }); + + it('sets the geoip fields', async () => { + const eventWithBothIPs = await searchForID( + networkIndexData.eventsInfo[0]._id + ); + // Should be 'United States' + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo?.country_name).to.not.be( + undefined + ); + // should be 'Iceland' + expect(eventWithBothIPs.body.hits.hits[0]._source.destination.geo?.country_name).to.not.be( + undefined + ); + + const eventWithSourceOnly = await searchForID( + networkIndexData.eventsInfo[1]._id + ); + // Should be 'United States' + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo?.country_name).to.not.be( + undefined + ); + expect(eventWithSourceOnly.body.hits.hits[0]._source.destination?.geo).to.be(undefined); + }); + + it('does not set geoip fields for events in indices other than the network index', async () => { + const eventWithBothIPs = await searchForID( + processIndexData.eventsInfo[0]._id + ); + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo).to.be(undefined); + expect(eventWithBothIPs.body.hits.hits[0]._source.destination.geo).to.be(undefined); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index 4f2a801377204..231871fae3d39 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { SearchResponse } from 'elasticsearch'; import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; import { ResolverTree, @@ -20,7 +19,6 @@ import { InsertedEvents } from '../../services/resolver'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const resolver = getService('resolverGenerator'); - const es = getService('es'); const generator = new EndpointDocGenerator('resolver'); describe('Resolver handling of entity ids', () => { @@ -38,26 +36,10 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC }); it('excludes events that have an empty entity_id field', async () => { - // first lets get the _id of the document using the parent.process.entity_id - // then we'll use the API to search for that specific document - const res = await es.search>({ - index: genData.indices[0], - body: { - query: { - bool: { - filter: [ - { - term: { 'process.parent.entity_id': origin.process.parent!.entity_id }, - }, - ], - }, - }, - }, - }); const { body }: { body: ResolverEntityIndex } = await supertest.get( // using the same indices value here twice to force the query parameter to be an array // for some reason using supertest's query() function doesn't construct a parsable array - `/api/endpoint/resolver/entity?_id=${res.body.hits.hits[0]._id}&indices=${eventsIndexPattern}&indices=${eventsIndexPattern}` + `/api/endpoint/resolver/entity?_id=${genData.eventsInfo[0]._id}&indices=${eventsIndexPattern}&indices=${eventsIndexPattern}` ); expect(body).to.be.empty(); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts index 335689b804d5b..7e4d4177affac 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts @@ -11,7 +11,7 @@ import { } from '../../../plugins/security_solution/common/endpoint/generate_data'; import { FtrProviderContext } from '../ftr_provider_context'; -const processIndex = 'logs-endpoint.events.process-default'; +export const processEventsIndex = 'logs-endpoint.events.process-default'; /** * Options for build a resolver tree @@ -36,7 +36,7 @@ export interface GeneratedTrees { * Structure containing the events inserted into ES and the index they live in */ export interface InsertedEvents { - events: Event[]; + eventsInfo: Array<{ _id: string; event: Event }>; indices: string[]; } @@ -46,24 +46,37 @@ interface BulkCreateHeader { }; } +interface BulkResponse { + items: Array<{ + create: { + _id: string; + }; + }>; +} + export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { const client = getService('es'); return { async insertEvents( events: Event[], - eventsIndex: string = processIndex + eventsIndex: string = processEventsIndex ): Promise { const body = events.reduce((array: Array, doc) => { array.push({ create: { _index: eventsIndex } }, doc); return array; }, []); - await client.bulk({ body, refresh: true }); - return { events, indices: [eventsIndex] }; + const bulkResp = await client.bulk({ body, refresh: true }); + + const eventsInfo = events.map((event: Event, i: number) => { + return { event, _id: bulkResp.body.items[i].create._id }; + }); + + return { eventsInfo, indices: [eventsIndex] }; }, async createTrees( options: Options, - eventsIndex: string = processIndex, + eventsIndex: string = processEventsIndex, alertsIndex: string = 'logs-endpoint.alerts-default' ): Promise { const seed = options.seed || 'resolver-seed'; From c21474b4ce029b820072b53f3908075404bccbde Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Thu, 30 Jul 2020 14:51:35 -0400 Subject: [PATCH 04/55] [Security Solution][Detections] Change from sha1 to sha256 (#73741) --- .../exceptions/add_exception_modal/index.tsx | 3 +- .../exceptions/edit_exception_modal/index.tsx | 3 +- .../exceptions/exceptionable_fields.json | 26 +++-------- .../components/exceptions/helpers.test.tsx | 45 +++++++++++++++++++ .../common/components/exceptions/helpers.tsx | 36 +++++++++++++-- .../alerts_table/default_config.tsx | 2 +- 6 files changed, 88 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index bb547f05090b7..e6eaa4947e404 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -40,6 +40,7 @@ import { AddExceptionComments } from '../add_exception_comments'; import { enrichNewExceptionItemsWithComments, enrichExceptionItemsWithOS, + lowercaseHashValues, defaultEndpointExceptionItems, entryHasListType, entryHasNonEcsType, @@ -256,7 +257,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ : exceptionItemsToAdd; if (exceptionListType === 'endpoint') { const osTypes = retrieveAlertOsTypes(); - enriched = enrichExceptionItemsWithOS(enriched, osTypes); + enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; }, [comment, exceptionItemsToAdd, exceptionListType, retrieveAlertOsTypes]); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 341d2f2bab37a..6109b85f2da5a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -40,6 +40,7 @@ import { getOperatingSystems, entryHasListType, entryHasNonEcsType, + lowercaseHashValues, } from '../helpers'; import { Loader } from '../../loader'; @@ -195,7 +196,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ ]; if (exceptionListType === 'endpoint') { const osTypes = exceptionItem._tags ? getOperatingSystems(exceptionItem._tags) : []; - enriched = enrichExceptionItemsWithOS(enriched, osTypes); + enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; }, [exceptionItemsToAdd, exceptionItem, comment, exceptionListType]); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json index fdf0ea60ecf6a..037e340ee7fa2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json @@ -6,32 +6,25 @@ "Target.process.Ext.code_signature.valid", "Target.process.Ext.services", "Target.process.Ext.user", - "Target.process.command_line", "Target.process.command_line.text", - "Target.process.executable", "Target.process.executable.text", "Target.process.hash.md5", "Target.process.hash.sha1", "Target.process.hash.sha256", "Target.process.hash.sha512", - "Target.process.name", "Target.process.name.text", "Target.process.parent.Ext.code_signature.status", "Target.process.parent.Ext.code_signature.subject_name", "Target.process.parent.Ext.code_signature.trusted", "Target.process.parent.Ext.code_signature.valid", - "Target.process.parent.command_line", "Target.process.parent.command_line.text", - "Target.process.parent.executable", "Target.process.parent.executable.text", "Target.process.parent.hash.md5", "Target.process.parent.hash.sha1", "Target.process.parent.hash.sha256", "Target.process.parent.hash.sha512", - "Target.process.parent.name", "Target.process.parent.name.text", "Target.process.parent.pgid", - "Target.process.parent.working_directory", "Target.process.parent.working_directory.text", "Target.process.pe.company", "Target.process.pe.description", @@ -39,7 +32,6 @@ "Target.process.pe.original_file_name", "Target.process.pe.product", "Target.process.pgid", - "Target.process.working_directory", "Target.process.working_directory.text", "agent.id", "agent.type", @@ -74,7 +66,6 @@ "file.mode", "file.name", "file.owner", - "file.path", "file.path.text", "file.pe.company", "file.pe.description", @@ -82,7 +73,6 @@ "file.pe.original_file_name", "file.pe.product", "file.size", - "file.target_path", "file.target_path.text", "file.type", "file.uid", @@ -94,10 +84,8 @@ "host.id", "host.os.Ext.variant", "host.os.family", - "host.os.full", "host.os.full.text", "host.os.kernel", - "host.os.name", "host.os.name.text", "host.os.platform", "host.os.version", @@ -108,32 +96,25 @@ "process.Ext.code_signature.valid", "process.Ext.services", "process.Ext.user", - "process.command_line", "process.command_line.text", - "process.executable", "process.executable.text", "process.hash.md5", "process.hash.sha1", "process.hash.sha256", "process.hash.sha512", - "process.name", "process.name.text", "process.parent.Ext.code_signature.status", "process.parent.Ext.code_signature.subject_name", "process.parent.Ext.code_signature.trusted", "process.parent.Ext.code_signature.valid", - "process.parent.command_line", "process.parent.command_line.text", - "process.parent.executable", "process.parent.executable.text", "process.parent.hash.md5", "process.parent.hash.sha1", "process.parent.hash.sha256", "process.parent.hash.sha512", - "process.parent.name", "process.parent.name.text", "process.parent.pgid", - "process.parent.working_directory", "process.parent.working_directory.text", "process.pe.company", "process.pe.description", @@ -141,7 +122,10 @@ "process.pe.original_file_name", "process.pe.product", "process.pgid", - "process.working_directory", "process.working_directory.text", - "rule.uuid" + "rule.uuid", + "user.domain", + "user.email", + "user.hash", + "user.id" ] \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 5cb65ee6db8ff..18b509d16b352 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -24,6 +24,7 @@ import { entryHasListType, entryHasNonEcsType, prepareExceptionItemsForBulkClose, + lowercaseHashValues, } from './helpers'; import { EmptyEntry } from './types'; import { @@ -663,4 +664,48 @@ describe('Exception helpers', () => { expect(result).toEqual(expected); }); }); + + describe('#lowercaseHashValues', () => { + test('it should return an empty array with an empty array', () => { + const payload: ExceptionListItemSchema[] = []; + const result = lowercaseHashValues(payload); + expect(result).toEqual([]); + }); + + test('it should return all list items with entry hashes lowercased', () => { + const payload = [ + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'DDDFFF' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'aaabbb' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'user.hash', type: 'match_any', value: ['aaabbb', 'DDDFFF'] }, + ] as EntriesArray, + }, + ]; + const result = lowercaseHashValues(payload); + expect(result).toEqual([ + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'dddfff' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'aaabbb' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'user.hash', type: 'match_any', value: ['aaabbb', 'dddfff'] }, + ] as EntriesArray, + }, + ]); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 3abb788312ff4..2b526ede12acf 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -335,6 +335,36 @@ export const enrichExceptionItemsWithOS = ( }); }; +/** + * Returns given exceptionItems with all hash-related entries lowercased + */ +export const lowercaseHashValues = ( + exceptionItems: Array +): Array => { + return exceptionItems.map((item) => { + const newEntries = item.entries.map((itemEntry) => { + if (itemEntry.field.includes('.hash')) { + if (itemEntry.type === 'match') { + return { + ...itemEntry, + value: itemEntry.value.toLowerCase(), + }; + } else if (itemEntry.type === 'match_any') { + return { + ...itemEntry, + value: itemEntry.value.map((val) => val.toLowerCase()), + }; + } + } + return itemEntry; + }); + return { + ...item, + entries: newEntries, + }; + }); +}; + /** * Returns the value for the given fieldname within TimelineNonEcsData if it exists */ @@ -413,7 +443,7 @@ export const defaultEndpointExceptionItems = ( data: alertData, fieldName: 'file.Ext.code_signature.trusted', }); - const [sha1Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha1' }); + const [sha256Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha256' }); const [eventCode] = getMappedNonEcsValue({ data: alertData, fieldName: 'event.code' }); const namespaceType = 'agnostic'; @@ -446,10 +476,10 @@ export const defaultEndpointExceptionItems = ( value: filePath ?? '', }, { - field: 'file.hash.sha1', + field: 'file.hash.sha256', operator: 'included', type: 'match', - value: sha1Hash ?? '', + value: sha256Hash ?? '', }, { field: 'event.code', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 010129d2d4593..f38a9107afca9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -202,7 +202,7 @@ export const requiredFieldsForActions = [ 'file.path', 'file.Ext.code_signature.subject_name', 'file.Ext.code_signature.trusted', - 'file.hash.sha1', + 'file.hash.sha256', 'host.os.family', 'event.code', ]; From c670afab5ac3557b1aaa4adc66e152a176960cd6 Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Thu, 30 Jul 2020 16:00:24 -0400 Subject: [PATCH 05/55] Handle promise rejections when building artifacts (#73831) Co-authored-by: Elastic Machine --- .../manifest_manager/manifest_manager.mock.ts | 21 +++++++++++++++---- .../manifest_manager/manifest_manager.test.ts | 11 +++++++++- .../manifest_manager/manifest_manager.ts | 11 +++++----- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index 592ffb0eae62a..34e18c5fe85fc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -8,6 +8,7 @@ import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks import { Logger } from 'src/core/server'; import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; +import { ExceptionListClient } from '../../../../../../lists/server'; import { listMock } from '../../../../../../lists/server/mocks'; import LRU from 'lru-cache'; import { getArtifactClientMock } from '../artifact_client.mock'; @@ -23,22 +24,29 @@ import { export enum ManifestManagerMockType { InitialSystemState, + ListClientPromiseRejection, NormalFlow, } export const getManifestManagerMock = (opts?: { mockType?: ManifestManagerMockType; cache?: LRU; + exceptionListClient?: ExceptionListClient; packageConfigService?: jest.Mocked; savedObjectsClient?: ReturnType; }): ManifestManager => { let cache = new LRU({ max: 10, maxAge: 1000 * 60 * 60 }); - if (opts?.cache !== undefined) { + if (opts?.cache != null) { cache = opts.cache; } + let exceptionListClient = listMock.getExceptionListClient(); + if (opts?.exceptionListClient != null) { + exceptionListClient = opts.exceptionListClient; + } + let packageConfigService = createPackageConfigServiceMock(); - if (opts?.packageConfigService !== undefined) { + if (opts?.packageConfigService != null) { packageConfigService = opts.packageConfigService; } packageConfigService.list = jest.fn().mockResolvedValue({ @@ -51,7 +59,7 @@ export const getManifestManagerMock = (opts?: { }); let savedObjectsClient = savedObjectsClientMock.create(); - if (opts?.savedObjectsClient !== undefined) { + if (opts?.savedObjectsClient != null) { savedObjectsClient = opts.savedObjectsClient; } @@ -61,6 +69,11 @@ export const getManifestManagerMock = (opts?: { switch (mockType) { case ManifestManagerMockType.InitialSystemState: return getEmptyMockArtifacts(); + case ManifestManagerMockType.ListClientPromiseRejection: + exceptionListClient.findExceptionListItem = jest + .fn() + .mockRejectedValue(new Error('unexpected thing happened')); + return super.buildExceptionListArtifacts('v1'); case ManifestManagerMockType.NormalFlow: return getMockArtifactsWithDiff(); } @@ -85,7 +98,7 @@ export const getManifestManagerMock = (opts?: { artifactClient: getArtifactClientMock(savedObjectsClient), cache, packageConfigService, - exceptionListClient: listMock.getExceptionListClient(), + exceptionListClient, logger: loggingSystemMock.create().get() as jest.Mocked, savedObjectsClient, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index d99d6a959d7aa..8e0d55104fb7c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -9,7 +9,7 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; import { ArtifactConstants, ManifestConstants, isCompleteArtifact } from '../../../lib/artifacts'; -import { getManifestManagerMock } from './manifest_manager.mock'; +import { getManifestManagerMock, ManifestManagerMockType } from './manifest_manager.mock'; import LRU from 'lru-cache'; describe('manifest_manager', () => { @@ -204,5 +204,14 @@ describe('manifest_manager', () => { oldArtifactId ); }); + + test('ManifestManager handles promise rejections when building artifacts', async () => { + // This test won't fail on an unhandled promise rejection, but it will cause + // an UnhandledPromiseRejectionWarning to be printed. + const manifestManager = getManifestManagerMock({ + mockType: ManifestManagerMockType.ListClientPromiseRejection, + }); + await expect(manifestManager.buildNewManifest()).rejects.toThrow(); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 217fd6de2ba68..7d700cdffc60d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -82,18 +82,17 @@ export class ManifestManager { protected async buildExceptionListArtifacts( artifactSchemaVersion?: string ): Promise { - return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce< - Promise - >(async (acc, os) => { + const artifacts: InternalArtifactCompleteSchema[] = []; + for (const os of ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS) { const exceptionList = await getFullEndpointExceptionList( this.exceptionListClient, os, artifactSchemaVersion ?? 'v1' ); - const artifacts = await acc; const artifact = await buildArtifact(exceptionList, os, artifactSchemaVersion ?? 'v1'); - return Promise.resolve([...artifacts, artifact]); - }, Promise.resolve([])); + artifacts.push(artifact); + } + return artifacts; } /** From 0d8846d374f353f5473025b1a66b683b74df3fbf Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Thu, 30 Jul 2020 13:12:30 -0700 Subject: [PATCH 06/55] [APM] fixes linking errors to ML and Discover (#73758) * Closes #73755 by removing the extra uri encoding from time ranges and using correct refreshValue rison types rather than just strings * removes commented out test assertion --- .../DiscoverLinks.integration.test.tsx | 16 ++++----- .../MachineLearningLinks/MLJobLink.test.tsx | 4 +-- .../MachineLearningLinks/MLLink.test.tsx | 2 +- .../useTimeSeriesExplorerHref.test.ts | 34 +++++++++++++++++++ .../useTimeSeriesExplorerHref.ts | 5 ++- .../shared/Links/rison_helpers.test.ts | 27 +++++++++++++++ .../components/shared/Links/rison_helpers.ts | 14 ++++---- .../__test__/sections.test.ts | 6 ++-- 8 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts create mode 100644 x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx index 359468073f7f4..ca02abc395992 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx @@ -33,8 +33,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'processor.event:"transaction" AND transaction.id:"8b60bd32ecc6e150" AND trace.id:"8b60bd32ecc6e1506735a8b6cfcf175c"'))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'processor.event:\\"transaction\\" AND transaction.id:\\"8b60bd32ecc6e150\\" AND trace.id:\\"8b60bd32ecc6e1506735a8b6cfcf175c\\"'))"` ); }); @@ -50,8 +50,8 @@ describe('DiscoverLinks', () => { '?rangeFrom=now/w&rangeTo=now&refreshPaused=true&refreshInterval=0', } as Location); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'span.id:"test-span-id"'))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'span.id:\\"test-span-id\\"'))"` ); }); @@ -72,8 +72,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:"service-name" AND error.grouping_key:"grouping-key"'),sort:('@timestamp':desc))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\"'),sort:('@timestamp':desc))"` ); }); @@ -95,8 +95,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:"service-name" AND error.grouping_key:"grouping-key" AND some:kuery-string'),sort:('@timestamp':desc))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\" AND some:kuery-string'),sort:('@timestamp':desc))"` ); }); }); 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 39082c2639a2c..9ba4aab0e23d9 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( - `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))"` + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now-4h))"` ); }); it('should produce the correct URL with jobId, serviceName, and transactionType', async () => { @@ -41,7 +41,7 @@ describe('MLJobLink', () => { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request)))"` + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request),zoom:(from:now/w,to:now-4h)))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index b4187b2f797ab..da345e35c10b1 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -21,6 +21,6 @@ test('MLLink produces the correct URL', async () => { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/some/path?_g=(ml:(jobIds:!(something)),refreshInterval:(pause:true,value:'0'),time:(from:now-5h,to:now-2h))"` + `"/basepath/app/ml#/some/path?_g=(ml:(jobIds:!(something)),refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` ); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts new file mode 100644 index 0000000000000..28daae7fd830e --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useTimeSeriesExplorerHref } from './useTimeSeriesExplorerHref'; + +jest.mock('../../../../hooks/useApmPluginContext', () => ({ + useApmPluginContext: () => ({ + core: { http: { basePath: { prepend: (url: string) => url } } }, + }), +})); + +jest.mock('../../../../hooks/useLocation', () => ({ + useLocation: () => ({ + search: + '?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true', + }), +})); + +describe('useTimeSeriesExplorerHref', () => { + it('correctly encodes time range values', async () => { + const href = useTimeSeriesExplorerHref({ + jobId: 'apm-production-485b-high_mean_transaction_duration', + serviceName: 'opbeans-java', + transactionType: 'request', + }); + + 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')))"` + ); + }); +}); 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 625b9205b6ce0..459ee8f0282ff 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 @@ -22,12 +22,14 @@ export function useTimeSeriesExplorerHref({ }) { const { core } = useApmPluginContext(); const location = useLocation(); + const { time, refreshInterval } = getTimepickerRisonData(location.search); const search = querystring.stringify( { _g: rison.encode({ ml: { jobIds: [jobId] }, - ...getTimepickerRisonData(location.search), + time, + refreshInterval, }), ...(serviceName && transactionType ? { @@ -37,6 +39,7 @@ export function useTimeSeriesExplorerHref({ 'service.name': serviceName, 'transaction.type': transactionType, }, + zoom: time, }, }), } diff --git a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts new file mode 100644 index 0000000000000..8dd662339b61a --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getTimepickerRisonData } from './rison_helpers'; + +describe('getTimepickerRisonData', () => { + it('returns object of timepicker range and refresh interval values', async () => { + const locationSearch = `?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true`; + const timepickerValues = getTimepickerRisonData(locationSearch); + + expect(timepickerValues).toMatchInlineSnapshot(` + Object { + "refreshInterval": Object { + "pause": true, + "value": 10000, + }, + "time": Object { + "from": "2020-07-29T17:27:29.000Z", + "to": "2020-07-29T18:45:00.000Z", + }, + } + `); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts index 8b4d891dba83b..cab822b42be56 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts @@ -22,18 +22,16 @@ export function getTimepickerRisonData(currentSearch: Location['search']) { const currentQuery = toQuery(currentSearch); return { time: { - from: currentQuery.rangeFrom - ? encodeURIComponent(currentQuery.rangeFrom) - : '', - to: currentQuery.rangeTo ? encodeURIComponent(currentQuery.rangeTo) : '', + from: currentQuery.rangeFrom || '', + to: currentQuery.rangeTo || '', }, refreshInterval: { pause: currentQuery.refreshPaused - ? String(currentQuery.refreshPaused) - : '', + ? Boolean(currentQuery.refreshPaused) + : true, value: currentQuery.refreshInterval - ? String(currentQuery.refreshInterval) - : '', + ? parseInt(currentQuery.refreshInterval, 10) + : 0, }, }; } diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts index 186fc082ce5fe..e057e9c034615 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts @@ -68,7 +68,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], @@ -139,7 +139,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], @@ -209,7 +209,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], From 83fab293bc8c15660249b8d449e36e7b806056b6 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 30 Jul 2020 13:20:07 -0700 Subject: [PATCH 07/55] [DOCS] Fixes typo in Alerting actions (#73756) --- .../alerting-getting-started.asciidoc | 202 +++++++++++++++++ docs/user/alerting/index.asciidoc | 203 +----------------- 2 files changed, 203 insertions(+), 202 deletions(-) create mode 100644 docs/user/alerting/alerting-getting-started.asciidoc diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc new file mode 100644 index 0000000000000..6bc085b0f78b9 --- /dev/null +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -0,0 +1,202 @@ +[role="xpack"] +[[alerting-getting-started]] += Alerting and Actions + +beta[] + +-- + +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. + +image::images/alerting-overview.png[Alerts and actions UI] + +[IMPORTANT] +============================================== +To make sure you can access alerting and actions, see the <> section. +============================================== + +[float] +== Concepts and terminology + +*Alerts* work by running checks on a schedule to detect conditions. When a condition is met, the alert tracks it as an *alert instance* and responds by triggering one or more *actions*. +Actions typically involve interaction with {kib} services or third party integrations. *Connectors* allow actions to talk to these services and integrations. +This section describes all of these elements and how they operate together. + +[float] +=== What is an alert? + +An alert specifies a background task that runs on the {kib} server to check for specific conditions. It consists of three main parts: + +* *Conditions*: what needs to be detected? +* *Schedule*: when/how often should detection checks run? +* *Actions*: what happens when a condition is detected? + +For example, when monitoring a set of servers, an alert might check for average CPU usage > 0.9 on each server for the two minutes (condition), checked every minute (schedule), sending a warning email message via SMTP with subject `CPU on {{server}} is high` (action). + +image::images/what-is-an-alert.svg[Three components of an alert] + +The following sections each part of the alert is described in more detail. + +[float] +[[alerting-concepts-conditions]] +==== Conditions + +Under the hood, {kib} alerts detect conditions by running javascript function on the {kib} server, which gives it flexibility to support a wide range of detections, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. + +These detections are packaged and exposed as *alert types*. An alert type hides the underlying details of the detection, and exposes a set of parameters +to control the details of the conditions to detect. + +For example, an <> lets you specify the index to query, an aggregation field, and a time window, but the details of the underlying {es} query are hidden. + +See <> for the types of alerts provided by {kib} and how they express their conditions. + +[float] +[[alerting-concepts-scheduling]] +==== Schedule + +Alert schedules are defined as an interval between subsequent checks, and can range from a few seconds to months. + +[IMPORTANT] +============================================== +The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. +============================================== + +[float] +[[alerting-concepts-actions]] +==== Actions + +Actions are invocations of {kib} services or integrations with third-party systems, that run as background tasks on the {kib} server when alert conditions are met. + +When defining actions in an alert, you specify: + +* the *action type*: the type of service or integration to use +* the connection for that type by referencing a <> +* a mapping of alert values to properties exposed for that type of action + +The result is a template: all the parameters needed to invoke a service are supplied except for specific values that are only known at the time the alert condition is detected. + +In the server monitoring example, the `email` action type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. + +When the alert detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` action type. + +image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] + +See <> for details on the types of actions provided by {kib}. + +[float] +[[alerting-concepts-alert-instances]] +=== Alert instances + +When checking for a condition, an alert might identify multiple occurrences of the condition. {kib} tracks each of these *alert instances* separately and takes action per instance. + +Using the server monitoring example, each server with average CPU > 0.9 is tracked as an alert instance. This means a separate email is sent for each server that exceeds the threshold. + +image::images/alert-instances.svg[{kib} tracks each detected condition as an alert instance and takes action on each instance] + +[float] +[[alerting-concepts-suppressing-duplicate-notifications]] +=== Suppressing duplicate notifications + +Since actions are taken per instance, alerts can end up generating a large number of actions. Take the following example where an alert is monitoring three servers every minute for CPU usage > 0.9: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, on for X123 and one for Y456. +* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. + +In the above example, three emails are sent for server X123 in the span of 3 minutes for the same condition. Often it's desirable to suppress frequent re-notification. Operations like muting and re-notification throttling can be applied at the instance level. If we set the alert re-notify interval to 5 minutes, we reduce noise by only getting emails for new servers that exceed the threshold: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456 +* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. + +[float] +[[alerting-concepts-connectors]] +=== Connectors + +Actions often involve connecting with services inside {kib} or integrations with third-party systems. +Rather than repeatedly entering connection information and credentials for each action, {kib} simplifies action setup using *connectors*. + +*Connectors* provide a central place to store connection information for services and integrations. For example if four alerts send email notifications via the same SMTP service, +they all reference the same SMTP connector. When the SMTP settings change they are updated once in the connector, instead of having to update four alerts. + +image::images/alert-concepts-connectors.svg[Connectors provide a central place to store service connection settings] + +[float] +=== Summary + +An _alert_ consists of conditions, _actions_, and a schedule. When conditions are met, _alert instances_ are created that render _actions_ and invoke them. To make action setup and update easier, actions refer to _connectors_ that centralize the information used to connect with {kib} services and third-party integrations. + +image::images/alert-concepts-summary.svg[Alerts, actions, alert instances and connectors work together to convert detection into action] + +* *Alert*: a specification of the conditions to be detected, the schedule for detection, and the response when detection occurs. +* *Action*: the response to a detected condition defined in the alert. Typically actions specify a service or third party integration along with alert details that will be sent to it. +* *Alert instance*: state tracked by {kib} for every occurrence of a detected condition. Actions as well as controls like muting and re-notification are controlled at the instance level. +* *Connector*: centralized configurations for services and third party integration that are referenced by actions. + +[float] +[[alerting-concepts-differences]] +== Differences from Watcher + +{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems. + +This section will clarify some of the important differences in the function and intent of the two systems. + +Functionally, {kib} alerting differs in that: + +* Scheduled checks are run on {kib} instead of {es} +* {kib} <> through *alert types*, whereas watches provide low-level control over inputs, conditions, and transformations. +* {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. +* Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. + +At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. +Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. + +[float] +[[alerting-setup-prerequisites]] +== Setup and prerequisites + +If you are using an *on-premises* Elastic Stack deployment: + +* In the kibana.yml configuration file, add the <> setting. + +If you are using an *on-premises* Elastic Stack deployment with <>: + +* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. + +[float] +[[alerting-security]] +== Security + +To access alerting in a space, a user must have access to one of the following features: + +* <> +* <> +* <> +* <> + +See <> for more information on configuring roles that provide access to these features. + +[float] +[[alerting-spaces]] +=== Space isolation + +Alerts and connectors are isolated to the {kib} space in which they were created. An alert or connector created in one space will not be visible in another. + +[float] +[[alerting-authorization]] +=== Authorization + +Alerts, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the alert. Upon creating or modifying an alert, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the alert including detection checks and executing actions. + +[IMPORTANT] +============================================== +If an alert requires certain privileges to run such as index privileges, keep in mind that if a user without those privileges updates the alert, the alert will no longer function. +============================================== + +[float] +[[alerting-restricting-actions]] +=== Restricting actions + +For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and whitelist the hostnames that {kib} can connect with. + +-- \ No newline at end of file diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 6f691f2715bc8..56404d9a33b80 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -1,205 +1,4 @@ -[role="xpack"] -[[alerting-getting-started]] -= Alerting and Actions - -beta[] - --- - -Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. - -image::images/alerting-overview.png[Alerts and actions UI] - -[IMPORTANT] -============================================== -To make sure you can access alerting and actions, see the <> section. -============================================== - -[float] -== Concepts and terminology - -*Alerts* work by running checks on a schedule to detect conditions. When a condition is met, the alert tracks it as an *alert instance* and responds by triggering one or more *actions*. -Actions typically involve interaction with {kib} services or third party integrations. *Connectors* allow actions to talk to these services and integrations. -This section describes all of these elements and how they operate together. - -[float] -=== What is an alert? - -An alert specifies a background task that runs on the {kib} server to check for specific conditions. It consists of three main parts: - -* *Conditions*: what needs to be detected? -* *Schedule*: when/how often should detection checks run? -* *Actions*: what happens when a condition is detected? - -For example, when monitoring a set of servers, an alert might check for average CPU usage > 0.9 on each server for the two minutes (condition), checked every minute (schedule), sending a warning email message via SMTP with subject `CPU on {{server}} is high` (action). - -image::images/what-is-an-alert.svg[Three components of an alert] - -The following sections each part of the alert is described in more detail. - -[float] -[[alerting-concepts-conditions]] -==== Conditions - -Under the hood, {kib} alerts detect conditions by running javascript function on the {kib} server, which gives it flexibility to support a wide range of detections, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. - -These detections are packaged and exposed as *alert types*. An alert type hides the underlying details of the detection, and exposes a set of parameters -to control the details of the conditions to detect. - -For example, an <> lets you specify the index to query, an aggregation field, and a time window, but the details of the underlying {es} query are hidden. - -See <> for the types of alerts provided by {kib} and how they express their conditions. - -[float] -[[alerting-concepts-scheduling]] -==== Schedule - -Alert schedules are defined as an interval between subsequent checks, and can range from a few seconds to months. - -[IMPORTANT] -============================================== -The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. -============================================== - -[float] -[[alerting-concepts-actions]] -==== Actions - -Actions are invocations of {kib} services or integrations with third-party systems, that run as background tasks on the {kib} server when alert conditions are met. - -When defining actions in an alert, you specify -* the *action type*: the type of service or integration to use> -* the connection for that type by referencing a <>. -* a mapping of alert values to properties exposed for that type of action. - -The result is a template: all the parameters needed to invoke a service are supplied except for specific values that are only known at the time the alert condition is detected. - -In the server monitoring example, the `email` action type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. - -When the alert detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` action type. - -image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] - -See <> for details on the types of actions provided by {kib}. - -[float] -[[alerting-concepts-alert-instances]] -=== Alert instances - -When checking for a condition, an alert might identify multiple occurrences of the condition. {kib} tracks each of these *alert instances* separately and takes action per instance. - -Using the server monitoring example, each server with average CPU > 0.9 is tracked as an alert instance. This means a separate email is sent for each server that exceeds the threshold. - -image::images/alert-instances.svg[{kib} tracks each detected condition as an alert instance and takes action on each instance] - -[float] -[[alerting-concepts-suppressing-duplicate-notifications]] -=== Suppressing duplicate notifications - -Since actions are taken per instance, alerts can end up generating a large number of actions. Take the following example where an alert is monitoring three servers every minute for CPU usage > 0.9: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, on for X123 and one for Y456. -* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. - -In the above example, three emails are sent for server X123 in the span of 3 minutes for the same condition. Often it's desirable to suppress frequent re-notification. Operations like muting and re-notification throttling can be applied at the instance level. If we set the alert re-notify interval to 5 minutes, we reduce noise by only getting emails for new servers that exceed the threshold: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456 -* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. - -[float] -[[alerting-concepts-connectors]] -=== Connectors - -Actions often involve connecting with services inside {kib} or integrations with third-party systems. -Rather than repeatedly entering connection information and credentials for each action, {kib} simplifies action setup using *connectors*. - -*Connectors* provide a central place to store connection information for services and integrations. For example if four alerts send email notifications via the same SMTP service, -they all reference the same SMTP connector. When the SMTP settings change they are updated once in the connector, instead of having to update four alerts. - -image::images/alert-concepts-connectors.svg[Connectors provide a central place to store service connection settings] - -[float] -=== Summary - -An _alert_ consists of conditions, _actions_, and a schedule. When conditions are met, _alert instances_ are created that render _actions_ and invoke them. To make action setup and update easier, actions refer to _connectors_ that centralize the information used to connect with {kib} services and third-party integrations. - -image::images/alert-concepts-summary.svg[Alerts, actions, alert instances and connectors work together to convert detection into action] - -* *Alert*: a specification of the conditions to be detected, the schedule for detection, and the response when detection occurs. -* *Action*: the response to a detected condition defined in the alert. Typically actions specify a service or third party integration along with alert details that will be sent to it. -* *Alert instance*: state tracked by {kib} for every occurrence of a detected condition. Actions as well as controls like muting and re-notification are controlled at the instance level. -* *Connector*: centralized configurations for services and third party integration that are referenced by actions. - -[float] -[[alerting-concepts-differences]] -== Differences from Watcher - -{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems. - -This section will clarify some of the important differences in the function and intent of the two systems. - -Functionally, {kib} alerting differs in that: - -* Scheduled checks are run on {kib} instead of {es} -* {kib} <> through *alert types*, whereas watches provide low-level control over inputs, conditions, and transformations. -* {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. -* Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. - -At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. -Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. - -[float] -[[alerting-setup-prerequisites]] -== Setup and prerequisites - -If you are using an *on-premises* Elastic Stack deployment: - -* In the kibana.yml configuration file, add the <> setting. - -If you are using an *on-premises* Elastic Stack deployment with <>: - -* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. - -[float] -[[alerting-security]] -== Security - -To access alerting in a space, a user must have access to one of the following features: - -* <> -* <> -* <> -* <> - -See <> for more information on configuring roles that provide access to these features. - -[float] -[[alerting-spaces]] -=== Space isolation - -Alerts and connectors are isolated to the {kib} space in which they were created. An alert or connector created in one space will not be visible in another. - -[float] -[[alerting-authorization]] -=== Authorization - -Alerts, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the alert. Upon creating or modifying an alert, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the alert including detection checks and executing actions. - -[IMPORTANT] -============================================== -If an alert requires certain privileges to run such as index privileges, keep in mind that if a user without those privileges updates the alert, the alert will no longer function. -============================================== - -[float] -[[alerting-restricting-actions]] -=== Restricting actions - -For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and whitelist the hostnames that {kib} can connect with. - --- - +include::alerting-getting-started.asciidoc[] include::defining-alerts.asciidoc[] include::action-types.asciidoc[] include::alert-types.asciidoc[] From a6888e369465ae65e8d1c5181c25dad1d50b0a43 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 30 Jul 2020 16:21:29 -0400 Subject: [PATCH 08/55] [Ingest Manager] Fix config selection in enrollment flyout from config list page (#73833) --- .../agent_enrollment_flyout/config_selection.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx index e98ebb7cadc7c..5343d86244f1e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx @@ -64,6 +64,14 @@ export const EnrollmentStepAgentConfig: React.FC = (props) => { useEffect( function useDefaultConfigEffect() { if (agentConfigs && agentConfigs.length && !selectedState.agentConfigId) { + if (agentConfigs.length === 1) { + setSelectedState({ + ...selectedState, + agentConfigId: agentConfigs[0].id, + }); + return; + } + const defaultConfig = agentConfigs.find((config) => config.is_default); if (defaultConfig) { setSelectedState({ From 5e86d2f848ee6b44fa42dd27c4aa00ddb3288e44 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Thu, 30 Jul 2020 13:44:37 -0700 Subject: [PATCH 09/55] Closes #72914 by hiding anomaly detection settings links when the ml plugin is disabled. (#73638) Co-authored-by: Elastic Machine --- .../apm/public/components/app/Home/index.tsx | 11 ++++--- .../anomaly_detection/add_environments.tsx | 2 +- .../app/Settings/anomaly_detection/index.tsx | 4 +-- .../public/components/app/Settings/index.tsx | 33 ++++++++++++------- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index b09c03f853aa9..c6c0861c26a34 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -83,7 +83,8 @@ interface Props { } export function Home({ tab }: Props) { - const { config } = useApmPluginContext(); + const { config, core } = useApmPluginContext(); + const isMLEnabled = !!core.application.capabilities.ml; const homeTabs = getHomeTabs(config); const selectedTab = homeTabs.find( (homeTab) => homeTab.name === tab @@ -105,9 +106,11 @@ export function Home({ tab }: Props) { - - - + {isMLEnabled && ( + + + + )} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index 48fb19560e43f..a594edb32b083 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -64,7 +64,7 @@ export function AddEnvironments({ return ( {ML_ERRORS.MISSING_WRITE_PRIVILEGES}} /> diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index f59949b22b3c8..9c04caf61022a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -29,7 +29,7 @@ const DEFAULT_VALUE: AnomalyDetectionApiResponse = { export function AnomalyDetection() { const plugin = useApmPluginContext(); - const canGetJobs = !!plugin.core.application.capabilities.ml.canGetJobs; + const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs; const license = useLicense(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); @@ -57,7 +57,7 @@ export function AnomalyDetection() { return ( {ML_ERRORS.MISSING_READ_PRIVILEGES}} /> diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index bd2ea706e492d..1471bc345d850 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -16,8 +16,11 @@ import { import { HomeLink } from '../../shared/Links/apm/HomeLink'; import { useLocation } from '../../../hooks/useLocation'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; export function Settings(props: { children: ReactNode }) { + const plugin = useApmPluginContext(); + const isMLEnabled = !!plugin.core.application.capabilities.ml; const { search, pathname } = useLocation(); return ( <> @@ -48,17 +51,25 @@ export function Settings(props: { children: ReactNode }) { '/settings/agent-configuration' ), }, - { - name: i18n.translate( - 'xpack.apm.settings.anomalyDetection', - { - defaultMessage: 'Anomaly detection', - } - ), - id: '4', - href: getAPMHref('/settings/anomaly-detection', search), - isSelected: pathname === '/settings/anomaly-detection', - }, + ...(isMLEnabled + ? [ + { + name: i18n.translate( + 'xpack.apm.settings.anomalyDetection', + { + defaultMessage: 'Anomaly detection', + } + ), + id: '4', + href: getAPMHref( + '/settings/anomaly-detection', + search + ), + isSelected: + pathname === '/settings/anomaly-detection', + }, + ] + : []), { name: i18n.translate('xpack.apm.settings.customizeApp', { defaultMessage: 'Customize app', From c2d8869ccac54a80a9bc239ad141920c3a7df09d Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 30 Jul 2020 15:52:32 -0500 Subject: [PATCH 10/55] [Metrics UI] Fix previewing of No Data results (#73753) --- .../common/components/alert_preview.tsx | 2 +- .../inventory/components/expression.tsx | 1 + .../evaluate_condition.ts | 8 ++-- .../inventory_metric_threshold_executor.ts | 9 ++-- ...review_inventory_metric_threshold_alert.ts | 41 ++++++++++--------- .../metric_threshold/lib/evaluate_alert.ts | 4 +- .../metric_threshold_executor.ts | 2 +- .../preview_metric_threshold_alert.ts | 30 +++++++------- .../infra/server/routes/alerting/preview.ts | 23 +++++++---- 9 files changed, 67 insertions(+), 53 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index f3136ca155c78..d5b50fce38718 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -194,7 +194,7 @@ export const AlertPreview: React.FC = (props) => { plural: previewResult.resultTotals.noData !== 1 ? 's' : '', }} /> - ) : null} + ) : null}{' '} {previewResult.resultTotals.error ? ( = (props) => { validate={validateMetricThreshold} fetch={alertsContext.http.fetch} groupByDisplayName={alertParams.nodeType} + showNoDataResults={alertParams.alertOnNoData} /> diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 5c31c78b10fa9..3b795810b39f0 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -23,9 +23,9 @@ import { InfraSourceConfiguration } from '../../sources'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; type ConditionResult = InventoryMetricConditions & { - shouldFire: boolean | boolean[]; + shouldFire: boolean[]; currentValue: number; - isNoData: boolean; + isNoData: boolean[]; isError: boolean; }; @@ -71,8 +71,8 @@ export const evaluateCondition = async ( value !== null && (Array.isArray(value) ? value.map((v) => comparisonFunction(Number(v), threshold)) - : comparisonFunction(value as number, threshold)), - isNoData: value === null, + : [comparisonFunction(value as number, threshold)]), + isNoData: Array.isArray(value) ? value.map((v) => v === null) : [value === null], isError: value === undefined, currentValue: getCurrentValue(value), }; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 60eee49a5010d..7b816f2f225b5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -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 { first, get } from 'lodash'; +import { first, get, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; @@ -56,11 +56,14 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = for (const item of inventoryItems) { const alertInstance = services.alertInstanceFactory(`${item}`); // AND logic; all criteria must be across the threshold - const shouldAlertFire = results.every((result) => result[item].shouldFire); + const shouldAlertFire = results.every((result) => + // Grab the result of the most recent bucket + last(result[item].shouldFire) + ); // AND logic; because we need to evaluate all criteria, if one of them reports no data then the // whole alert is in a No Data/Error state - const isNoData = results.some((result) => result[item].isNoData); + const isNoData = results.some((result) => last(result[item].isNoData)); const isError = results.some((result) => result[item].isError); const nextState = isError diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index 5c654e2f47e78..562f344dbd060 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -59,28 +59,29 @@ export const previewInventoryMetricThresholdAlert = async ({ const inventoryItems = Object.keys(first(results) as any); const previewResults = inventoryItems.map((item) => { - const isNoData = results.some((result) => result[item].isNoData); - if (isNoData) { - return null; - } - const isError = results.some((result) => result[item].isError); - if (isError) { - return undefined; - } - const numberOfResultBuckets = lookbackSize; const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution); - return [...Array(numberOfExecutionBuckets)].reduce( - (totalFired, _, i) => - totalFired + - (results.every((result) => { - const shouldFire = result[item].shouldFire as boolean[]; - return shouldFire[Math.floor(i * alertResultsPerExecution)]; - }) - ? 1 - : 0), - 0 - ); + let numberOfTimesFired = 0; + let numberOfNoDataResults = 0; + let numberOfErrors = 0; + for (let i = 0; i < numberOfExecutionBuckets; i++) { + const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); + const allConditionsFiredInMappedBucket = results.every((result) => { + const shouldFire = result[item].shouldFire as boolean[]; + return shouldFire[mappedBucketIndex]; + }); + const someConditionsNoDataInMappedBucket = results.some((result) => { + const hasNoData = result[item].isNoData as boolean[]; + return hasNoData[mappedBucketIndex]; + }); + const someConditionsErrorInMappedBucket = results.some((result) => { + return result[item].isError; + }); + if (allConditionsFiredInMappedBucket) numberOfTimesFired++; + if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++; + if (someConditionsErrorInMappedBucket) numberOfErrors++; + } + return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors]; }); return previewResults; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index ca46f6cc16547..49f82c7ccec0b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -72,7 +72,9 @@ export const evaluateAlert = ( typeof point.value === 'number' && comparisonFunction(point.value, threshold) ) : [false], - isNoData: (Array.isArray(points) ? last(points)?.value : points) === null, + isNoData: Array.isArray(points) + ? points.map((point) => point?.value === null || point === null) + : [points === null], isError: isNaN(Array.isArray(points) ? last(points)?.value : points), }; }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index b4754a8624fd5..b2a8f0281b9e2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -45,7 +45,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => ); // AND logic; because we need to evaluate all criteria, if one of them reports no data then the // whole alert is in a No Data/Error state - const isNoData = alertResults.some((result) => result[group].isNoData); + const isNoData = alertResults.some((result) => last(result[group].isNoData)); const isError = alertResults.some((result) => result[group].isError); const nextState = isError diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts index 0ecfa27d0f0a8..5aca7f0890940 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -36,7 +36,7 @@ export const previewMetricThresholdAlert: ( params: PreviewMetricThresholdAlertParams, iterations?: number, precalculatedNumberOfGroups?: number -) => Promise> = async ( +) => Promise = async ( { callCluster, params, @@ -77,15 +77,6 @@ export const previewMetricThresholdAlert: ( const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds; const previewResults = await Promise.all( groups.map(async (group) => { - const isNoData = alertResults.some((alertResult) => alertResult[group].isNoData); - if (isNoData) { - return null; - } - const isError = alertResults.some((alertResult) => alertResult[group].isError); - if (isError) { - return NaN; - } - // Interpolate the buckets returned by evaluateAlert and return a count of how many of these // buckets would have fired the alert. If the alert interval and bucket interval are the same, // this will be a 1:1 evaluation of the alert results. If these are different, the interpolation @@ -95,14 +86,25 @@ export const previewMetricThresholdAlert: ( numberOfResultBuckets / alertResultsPerExecution ); let numberOfTimesFired = 0; + let numberOfNoDataResults = 0; + let numberOfErrors = 0; for (let i = 0; i < numberOfExecutionBuckets; i++) { const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); const allConditionsFiredInMappedBucket = alertResults.every( (alertResult) => alertResult[group].shouldFire[mappedBucketIndex] ); + const someConditionsNoDataInMappedBucket = alertResults.some((alertResult) => { + const hasNoData = alertResult[group].isNoData as boolean[]; + return hasNoData[mappedBucketIndex]; + }); + const someConditionsErrorInMappedBucket = alertResults.some((alertResult) => { + return alertResult[group].isError; + }); if (allConditionsFiredInMappedBucket) numberOfTimesFired++; + if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++; + if (someConditionsErrorInMappedBucket) numberOfErrors++; } - return numberOfTimesFired; + return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors]; }) ); return previewResults; @@ -152,9 +154,9 @@ export const previewMetricThresholdAlert: ( // so filter these results out entirely and only regard the resultA portion .filter((value) => typeof value !== 'undefined') .reduce((a, b) => { - if (typeof a !== 'number') return a; - if (typeof b !== 'number') return b; - return a + b; + if (!a) return b; + if (!b) return a; + return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; }) ); return zippedResult as any; diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index 8a3e9e4d0bedc..5594323d706de 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -55,10 +55,13 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) const numberOfGroups = previewResult.length; const resultTotals = previewResult.reduce( - (totals, groupResult) => { - if (groupResult === null) return { ...totals, noData: totals.noData + 1 }; - if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 }; - return { ...totals, fired: totals.fired + groupResult }; + (totals, [firedResult, noDataResult, errorResult]) => { + return { + ...totals, + fired: totals.fired + firedResult, + noData: totals.noData + noDataResult, + error: totals.error + errorResult, + }; }, { fired: 0, @@ -66,7 +69,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) error: 0, } ); - return response.ok({ body: alertPreviewSuccessResponsePayloadRT.encode({ numberOfGroups, @@ -86,10 +88,13 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) const numberOfGroups = previewResult.length; const resultTotals = previewResult.reduce( - (totals, groupResult) => { - if (groupResult === null) return { ...totals, noData: totals.noData + 1 }; - if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 }; - return { ...totals, fired: totals.fired + groupResult }; + (totals, [firedResult, noDataResult, errorResult]) => { + return { + ...totals, + fired: totals.fired + firedResult, + noData: totals.noData + noDataResult, + error: totals.error + errorResult, + }; }, { fired: 0, From 877f2e082971ecd855acbf70039bcd39ba7a85a8 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 30 Jul 2020 17:05:43 -0400 Subject: [PATCH 11/55] [Security Solution][Exceptions] Adds autocomplete workaround for .text fields (#73761) ## Summary This PR provides a workaround for the autocomplete service not providing suggestions when the selected field is `someField.text`. As endpoint exceptions will be largely using `.text` for now, wanted to still provide the autocomplete service. Updates to the autocomplete components were done after seeing some React errors that were popping up related to memory leaks. This is due to the use of `debounce` I believe. The calls were still executed even after the builder component was unmounted. This resulted in the subsequent calls from the autocomplete service not always going through (sometimes being canceled) when reopening the builder. Moved the filtering of endpoint fields to occur in the existing helper function so that we would still have access to the corresponding `keyword` field of `text` fields when formatting the entries for the builder. --- .../autocomplete/field_value_match.test.tsx | 1 - .../autocomplete/field_value_match.tsx | 15 +- .../field_value_match_any.test.tsx | 1 - .../autocomplete/field_value_match_any.tsx | 15 +- .../use_field_value_autocomplete.test.ts | 17 +- .../hooks/use_field_value_autocomplete.ts | 92 +++-- .../builder/builder_entry_item.test.tsx | 70 ++++ .../exceptions/builder/builder_entry_item.tsx | 16 +- .../builder/builder_exception_item.test.tsx | 24 +- .../exceptions/builder/helpers.test.tsx | 343 +++++++++++++++--- .../components/exceptions/builder/helpers.tsx | 70 +++- .../components/exceptions/builder/index.tsx | 17 +- .../common/components/exceptions/types.ts | 1 + 13 files changed, 540 insertions(+), 142 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx index 72467a62f57c1..998ed1f3351c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx @@ -232,7 +232,6 @@ describe('AutocompleteFieldMatchComponent', () => { fields, }, value: 'value 1', - signal: new AbortController().signal, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index 137f6803dc54e..dfb3761bb3497 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -72,14 +72,13 @@ export const AutocompleteFieldMatchComponent: React.FC { - const signal = new AbortController().signal; - - updateSuggestions({ - fieldSelected: selectedField, - value: `${searchVal}`, - patterns: indexPattern, - signal, - }); + if (updateSuggestions != null) { + updateSuggestions({ + fieldSelected: selectedField, + value: searchVal, + patterns: indexPattern, + }); + } }; const isValid = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx index f3f0f2e2a44b1..0a0281a9c4a51 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx @@ -232,7 +232,6 @@ describe('AutocompleteFieldMatchAnyComponent', () => { fields, }, value: 'value 1', - signal: new AbortController().signal, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx index 5a15c1f7238de..1952ef865e045 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx @@ -65,14 +65,13 @@ export const AutocompleteFieldMatchAnyComponent: React.FC { - const signal = new AbortController().signal; - - updateSuggestions({ - fieldSelected: selectedField, - value: `${searchVal}`, - patterns: indexPattern, - signal, - }); + if (updateSuggestions != null) { + updateSuggestions({ + fieldSelected: selectedField, + value: searchVal, + patterns: indexPattern, + }); + } }; const onCreateOption = (option: string) => onChange([...(selectedValue || []), option]); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts index def2a303f6038..a76b50d11a875 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts @@ -199,12 +199,17 @@ describe('useFieldValueAutocomplete', () => { await waitForNextUpdate(); await waitForNextUpdate(); - result.current[2]({ - fieldSelected: getField('@tags'), - value: 'hello', - patterns: stubIndexPatternWithFields, - signal: new AbortController().signal, - }); + expect(result.current[2]).not.toBeNull(); + + // Added check for typescripts sake, if null, + // would not reach below logic as test would stop above + if (result.current[2] != null) { + result.current[2]({ + fieldSelected: getField('@tags'), + value: 'hello', + patterns: stubIndexPatternWithFields, + }); + } await waitForNextUpdate(); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts index 541c0a8d3fbae..a53914da93f27 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts @@ -11,16 +11,13 @@ import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/d import { useKibana } from '../../../../common/lib/kibana'; import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; -export type UseFieldValueAutocompleteReturn = [ - boolean, - string[], - (args: { - fieldSelected: IFieldType | undefined; - value: string | string[] | undefined; - patterns: IIndexPattern | undefined; - signal: AbortSignal; - }) => void -]; +type Func = (args: { + fieldSelected: IFieldType | undefined; + value: string | string[] | undefined; + patterns: IIndexPattern | undefined; +}) => void; + +export type UseFieldValueAutocompleteReturn = [boolean, string[], Func | null]; export interface UseFieldValueAutocompleteProps { selectedField: IFieldType | undefined; @@ -41,62 +38,77 @@ export const useFieldValueAutocomplete = ({ const { services } = useKibana(); const [isLoading, setIsLoading] = useState(false); const [suggestions, setSuggestions] = useState([]); - const updateSuggestions = useRef( - debounce( + const updateSuggestions = useRef(null); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const fetchSuggestions = debounce( async ({ fieldSelected, value, patterns, - signal, }: { fieldSelected: IFieldType | undefined; value: string | string[] | undefined; patterns: IIndexPattern | undefined; - signal: AbortSignal; }) => { - if (fieldSelected == null || patterns == null) { - return; - } + const inputValue: string | string[] = value ?? ''; + const userSuggestion: string = Array.isArray(inputValue) + ? inputValue[inputValue.length - 1] ?? '' + : inputValue; - setIsLoading(true); + try { + if (isSubscribed) { + if (fieldSelected == null || patterns == null) { + return; + } - // Fields of type boolean should only display two options - if (fieldSelected.type === 'boolean') { - setIsLoading(false); - setSuggestions(['true', 'false']); - return; - } + setIsLoading(true); - const newSuggestions = await services.data.autocomplete.getValueSuggestions({ - indexPattern: patterns, - field: fieldSelected, - query: '', - signal, - }); + // Fields of type boolean should only display two options + if (fieldSelected.type === 'boolean') { + setIsLoading(false); + setSuggestions(['true', 'false']); + return; + } - setIsLoading(false); - setSuggestions(newSuggestions); + const newSuggestions = await services.data.autocomplete.getValueSuggestions({ + indexPattern: patterns, + field: fieldSelected, + query: userSuggestion.trim(), + signal: abortCtrl.signal, + }); + + setIsLoading(false); + setSuggestions([...newSuggestions]); + } + } catch (error) { + if (isSubscribed) { + setSuggestions([]); + setIsLoading(false); + } + } }, 500 - ) - ); - - useEffect(() => { - const abortCtrl = new AbortController(); + ); if (operatorType !== OperatorTypeEnum.EXISTS) { - updateSuggestions.current({ + fetchSuggestions({ fieldSelected: selectedField, value: fieldValue, patterns: indexPattern, - signal: abortCtrl.signal, }); } + updateSuggestions.current = fetchSuggestions; + return (): void => { + isSubscribed = false; abortCtrl.abort(); }; - }, [updateSuggestions, selectedField, operatorType, fieldValue, indexPattern]); + }, [services.data.autocomplete, selectedField, operatorType, fieldValue, indexPattern]); return [isLoading, suggestions, updateSuggestions.current]; }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx index 3dcc3eb5a8329..0f54ec29cc540 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx @@ -55,6 +55,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -81,6 +82,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -111,6 +113,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -143,6 +146,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -175,6 +179,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -207,6 +212,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -239,6 +245,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -271,6 +278,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -306,6 +314,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -331,6 +340,62 @@ describe('BuilderEntryItem', () => { ).toBeTruthy(); }); + test('it uses "correspondingKeywordField" if it exists', () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').prop('selectedField') + ).toEqual({ + name: 'extension', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); + }); + test('it invokes "onChange" when new field is selected and resets operator and value fields', () => { const mockOnChange = jest.fn(); const wrapper = mount( @@ -342,6 +407,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -376,6 +442,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -410,6 +477,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -444,6 +512,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -478,6 +547,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx index 5939a5a1b576e..3883a2fad2cf2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx @@ -95,7 +95,7 @@ export const BuilderEntryItem: React.FC = ({ const renderFieldInput = useCallback( (isFirst: boolean): JSX.Element => { - const filteredIndexPatterns = getFilteredIndexPatterns(indexPattern, entry); + const filteredIndexPatterns = getFilteredIndexPatterns(indexPattern, entry, listType); const comboBox = ( = ({ return comboBox; } }, - [handleFieldChange, indexPattern, entry] + [handleFieldChange, indexPattern, entry, listType] ); const renderOperatorInput = (isFirst: boolean): JSX.Element => { @@ -170,7 +170,11 @@ export const BuilderEntryItem: React.FC = ({ return ( = ({ return ( { + const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); + + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + autocomplete: { + getValueSuggestions: getValueSuggestionsMock, + }, + }, + }, + }); + }); + + afterEach(() => { + getValueSuggestionsMock.mockClear(); + }); + describe('and badge logic', () => { test('it renders "and" badge with extra top padding for the first exception item when "andLogicIncluded" is "true"', () => { const exceptionItem = { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx index 17c94adf42648..e8a5196a418d6 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx @@ -41,6 +41,7 @@ import { getOperatorOptions, getUpdatedEntriesOnDelete, isEntryNested, + getCorrespondingKeywordField, } from './helpers'; import { OperatorOption } from '../../autocomplete/types'; @@ -57,6 +58,7 @@ const getMockBuilderEntry = (): FormattedBuilderEntry => ({ nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }); const getMockNestedBuilderEntry = (): FormattedBuilderEntry => ({ @@ -73,6 +75,7 @@ const getMockNestedBuilderEntry = (): FormattedBuilderEntry => ({ parentIndex: 0, }, entryIndex: 0, + correspondingKeywordField: undefined, }); const getMockNestedParentBuilderEntry = (): FormattedBuilderEntry => ({ @@ -82,69 +85,305 @@ const getMockNestedParentBuilderEntry = (): FormattedBuilderEntry => ({ nested: 'parent', parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }); +const mockEndpointFields = [ + { + name: 'file.path.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'file.Ext.code_signature.status', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'file.Ext.code_signature' } }, + }, +]; + +export const getEndpointField = (name: string) => + mockEndpointFields.find((field) => field.name === name) as IFieldType; + describe('Exception builder helpers', () => { + describe('#getCorrespondingKeywordField', () => { + test('it returns matching keyword field if "selectedFieldIsTextType" is true and keyword field exists', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: 'machine.os.raw.text', + }); + + expect(output).toEqual(getField('machine.os.raw')); + }); + + test('it returns undefined if "selectedFieldIsTextType" is false', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: 'machine.os.raw', + }); + + expect(output).toEqual(undefined); + }); + + test('it returns undefined if "selectedField" is empty string', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: '', + }); + + expect(output).toEqual(undefined); + }); + + test('it returns undefined if "selectedField" is undefined', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: undefined, + }); + + expect(output).toEqual(undefined); + }); + }); + describe('#getFilteredIndexPatterns', () => { - test('it returns nested fields that match parent value when "item.nested" is "child"', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockNestedBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [ - { ...getField('nestedField.child') }, - { ...getField('nestedField.nestedChild.doublyNestedChild') }, - ], - id: '1234', - title: 'logstash-*', - }; - expect(output).toEqual(expected); + describe('list type detections', () => { + test('it returns nested fields that match parent value when "item.nested" is "child"', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockNestedBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [ + { ...getField('nestedField.child') }, + { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockNestedParentBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [{ ...getField('nestedField.child'), name: 'nestedField', esTypes: ['nested'] }], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [ + { ...getField('nestedField.child') }, + { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns all fields unfiletered if "item.nested" is not "child" or "parent"', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [...fields], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); }); - test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockNestedParentBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [{ ...getField('nestedField.child'), name: 'nestedField', esTypes: ['nested'] }], - id: '1234', - title: 'logstash-*', - }; - expect(output).toEqual(expected); + describe('list type endpoint', () => { + let payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + + beforeAll(() => { + payloadIndexPattern = { + ...payloadIndexPattern, + fields: [...payloadIndexPattern.fields, ...mockEndpointFields], + }; + }); + + test('it returns nested fields that match parent value when "item.nested" is "child"', () => { + const payloadItem: FormattedBuilderEntry = { + field: getEndpointField('file.Ext.code_signature.status'), + operator: isOperator, + value: 'some value', + nested: 'child', + parent: { + parent: { + ...getEntryNestedMock(), + field: 'file.Ext.code_signature', + entries: [{ ...getEntryMatchMock(), field: 'child' }], + }, + parentIndex: 0, + }, + entryIndex: 0, + correspondingKeywordField: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [getEndpointField('file.Ext.code_signature.status')], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: { + ...getEndpointField('file.Ext.code_signature.status'), + name: 'file.Ext.code_signature', + esTypes: ['nested'], + }, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [ + { + aggregatable: false, + count: 0, + esTypes: ['nested'], + name: 'file.Ext.code_signature', + readFromDocValues: false, + scripted: false, + searchable: true, + subType: { + nested: { + path: 'file.Ext.code_signature', + }, + }, + type: 'string', + }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [getEndpointField('file.Ext.code_signature.status')], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns all fields that matched those in "exceptionable_fields.json" with no further filtering if "item.nested" is not "child" or "parent"', () => { + const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [ + { + aggregatable: false, + count: 0, + esTypes: ['text'], + name: 'file.path.text', + readFromDocValues: false, + scripted: false, + searchable: true, + type: 'string', + }, + { + name: 'file.Ext.code_signature.status', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'file.Ext.code_signature' } }, + }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); }); + }); - test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = { - ...getMockNestedParentBuilderEntry(), - field: undefined, - }; - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { + describe('#getFormattedBuilderEntry', () => { + test('it returns entry with a value for "correspondingKeywordField" when "item.field" is of type "text" and matching keyword field exists', () => { + const payloadIndexPattern: IIndexPattern = { + ...getMockIndexPattern(), fields: [ - { ...getField('nestedField.child') }, - { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ...fields, + { + name: 'machine.os.raw.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: true, + }, ], - id: '1234', - title: 'logstash-*', }; - expect(output).toEqual(expected); - }); - - test('it returns all fields unfiletered if "item.nested" is not "child" or "parent"', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [...fields], - id: '1234', - title: 'logstash-*', + const payloadItem: BuilderEntry = { + ...getEntryMatchMock(), + field: 'machine.os.raw.text', + value: 'some os', + }; + const output = getFormattedBuilderEntry( + payloadIndexPattern, + payloadItem, + 0, + undefined, + undefined + ); + const expected: FormattedBuilderEntry = { + entryIndex: 0, + field: { + name: 'machine.os.raw.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: true, + }, + nested: undefined, + operator: isOperator, + parent: undefined, + value: 'some os', + correspondingKeywordField: getField('machine.os.raw'), }; expect(output).toEqual(expected); }); - }); - describe('#getFormattedBuilderEntry', () => { test('it returns "FormattedBuilderEntry" with value "nested" of "child" when "parent" and "parentIndex" are defined', () => { const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); const payloadItem: BuilderEntry = { ...getEntryMatchMock(), field: 'child' }; @@ -188,6 +427,7 @@ describe('Exception builder helpers', () => { parentIndex: 1, }, value: 'some host name', + correspondingKeywordField: undefined, }; expect(output).toEqual(expected); }); @@ -218,6 +458,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }; expect(output).toEqual(expected); }); @@ -252,6 +493,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some host name', + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); @@ -281,6 +523,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }, { entryIndex: 1, @@ -298,6 +541,7 @@ describe('Exception builder helpers', () => { operator: isOneOfOperator, parent: undefined, value: ['some extension'], + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); @@ -333,6 +577,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }, { entryIndex: 1, @@ -347,6 +592,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: undefined, + correspondingKeywordField: undefined, }, { entryIndex: 0, @@ -383,6 +629,7 @@ describe('Exception builder helpers', () => { parentIndex: 1, }, value: 'some host name', + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx index 93bae091885c1..8585f58504e31 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx @@ -33,6 +33,8 @@ import { EmptyNestedEntry, } from '../types'; import { getEntryValue, getExceptionOperatorSelect } from '../helpers'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import exceptionableFields from '../exceptionable_fields.json'; /** * Returns filtered index patterns based on the field - if a user selects to @@ -45,13 +47,21 @@ import { getEntryValue, getExceptionOperatorSelect } from '../helpers'; */ export const getFilteredIndexPatterns = ( patterns: IIndexPattern, - item: FormattedBuilderEntry + item: FormattedBuilderEntry, + type: ExceptionListType ): IIndexPattern => { + const indexPatterns = { + ...patterns, + fields: patterns.fields.filter(({ name }) => + type === 'endpoint' ? exceptionableFields.includes(name) : true + ), + }; + if (item.nested === 'child' && item.parent != null) { // when user has selected a nested entry, only fields with the common parent are shown return { - ...patterns, - fields: patterns.fields.filter( + ...indexPatterns, + fields: indexPatterns.fields.filter( (field) => field.subType != null && field.subType.nested != null && @@ -61,20 +71,53 @@ export const getFilteredIndexPatterns = ( }; } else if (item.nested === 'parent' && item.field != null) { // when user has selected a nested entry, right above it we show the common parent - return { ...patterns, fields: [item.field] }; + return { ...indexPatterns, fields: [item.field] }; } else if (item.nested === 'parent' && item.field == null) { // when user selects to add a nested entry, only nested fields are shown as options return { - ...patterns, - fields: patterns.fields.filter( + ...indexPatterns, + fields: indexPatterns.fields.filter( (field) => field.subType != null && field.subType.nested != null ), }; } else { - return patterns; + return indexPatterns; } }; +/** + * Fields of type 'text' do not generate autocomplete values, we want + * to find it's corresponding keyword type (if available) which does + * generate autocomplete values + * + * @param fields IFieldType fields + * @param selectedField the field name that was selected + * @param isTextType we only want a corresponding keyword field if + * the selected field is of type 'text' + * + */ +export const getCorrespondingKeywordField = ({ + fields, + selectedField, +}: { + fields: IFieldType[]; + selectedField: string | undefined; +}): IFieldType | undefined => { + const selectedFieldBits = + selectedField != null && selectedField !== '' ? selectedField.split('.') : []; + const selectedFieldIsTextType = selectedFieldBits.slice(-1)[0] === 'text'; + + if (selectedFieldIsTextType && selectedFieldBits.length > 0) { + const keywordField = selectedFieldBits.slice(0, selectedFieldBits.length - 1).join('.'); + const [foundKeywordField] = fields.filter( + ({ name }) => keywordField !== '' && keywordField === name + ); + return foundKeywordField; + } + + return undefined; +}; + /** * Formats the entry into one that is easily usable for the UI, most of the * complexity was introduced with nested fields @@ -95,11 +138,16 @@ export const getFormattedBuilderEntry = ( ): FormattedBuilderEntry => { const { fields } = indexPattern; const field = parent != null ? `${parent.field}.${item.field}` : item.field; - const [selectedField] = fields.filter(({ name }) => field != null && field === name); + const [foundField] = fields.filter(({ name }) => field != null && field === name); + const correspondingKeywordField = getCorrespondingKeywordField({ + fields, + selectedField: field, + }); if (parent != null && parentIndex != null) { return { - field: selectedField, + field: foundField, + correspondingKeywordField, operator: getExceptionOperatorSelect(item), value: getEntryValue(item), nested: 'child', @@ -108,7 +156,8 @@ export const getFormattedBuilderEntry = ( }; } else { return { - field: selectedField, + field: foundField, + correspondingKeywordField, operator: getExceptionOperatorSelect(item), value: getEntryValue(item), nested: undefined, @@ -167,6 +216,7 @@ export const getFormattedBuilderEntries = ( value: undefined, entryIndex: index, parent: undefined, + correspondingKeywordField: undefined, }; // User has selected to add a nested field, but not yet selected the field diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index 734434484fb4c..b82607a541aaa 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/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, { useCallback, useEffect, useMemo, useReducer } from 'react'; +import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; @@ -29,8 +29,6 @@ import { getDefaultEmptyEntry, getDefaultNestedEmptyEntry, } from './helpers'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import exceptionableFields from '../exceptionable_fields.json'; const MyInvisibleAndBadge = styled(EuiFlexItem)` visibility: hidden; @@ -244,17 +242,6 @@ export const ExceptionBuilder = ({ setUpdateExceptions([...exceptions, { ...newException }]); }, [setUpdateExceptions, exceptions, listType, listId, listNamespaceType, ruleName]); - // Filters index pattern fields by exceptionable fields if list type is endpoint - const filterIndexPatterns = useMemo((): IIndexPattern => { - if (listType === 'endpoint') { - return { - ...indexPatterns, - fields: indexPatterns.fields.filter(({ name }) => exceptionableFields.includes(name)), - }; - } - return indexPatterns; - }, [indexPatterns, listType]); - // The builder can have existing exception items, or new exception items that have yet // to be created (and thus lack an id), this was creating some React bugs with relying // on the index, as a result, created a temporary id when new exception items are first @@ -368,7 +355,7 @@ export const ExceptionBuilder = ({ key={getExceptionListItemId(exceptionListItem, index)} exceptionItem={exceptionListItem} exceptionId={getExceptionListItemId(exceptionListItem, index)} - indexPattern={filterIndexPatterns} + indexPattern={indexPatterns} listType={listType} addNested={addNested} exceptionItemIndex={index} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 83367e5b9e739..9b7c68848c41f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -62,6 +62,7 @@ export interface FormattedBuilderEntry { nested: 'parent' | 'child' | undefined; entryIndex: number; parent: { parent: EntryNested; parentIndex: number } | undefined; + correspondingKeywordField: IFieldType | undefined; } export interface EmptyEntry { From 744afcef7d9d5438485640a01588399d15e4b272 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 30 Jul 2020 17:22:10 -0400 Subject: [PATCH 12/55] [Security Solution][Exceptions] - Update how nested entries are displayed in exceptions viewer (#73745) ### Summary Small PR updating how nested entries are displayed in exception viewer. Doesn't change anything functionally, just cosmetic. --- .../viewer/exception_item/exception_entries.tsx | 16 ++++++++++++---- .../exceptions/viewer/helpers.test.tsx | 8 ++++---- .../components/exceptions/viewer/helpers.tsx | 5 +---- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx index 7069e99943f7b..bcc4adf54c9d7 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx @@ -52,6 +52,14 @@ const MyActionButton = styled(EuiFlexItem)` align-self: flex-end; `; +const MyNestedValueContainer = styled.div` + margin-left: ${({ theme }) => theme.eui.euiSizeL}; +`; + +const MyNestedValue = styled.span` + margin-left: ${({ theme }) => theme.eui.euiSizeS}; +`; + interface ExceptionEntriesComponentProps { entries: FormattedEntry[]; disableDelete: boolean; @@ -78,10 +86,10 @@ const ExceptionEntriesComponent = ({ render: (value: string | null, data: FormattedEntry) => { if (value != null && data.isNested) { return ( - <> - - {value} - + + + {value} + ); } else { return value ?? getEmptyValue(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx index 5d4340db9a448..5f6e54b0d3cff 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx @@ -106,13 +106,13 @@ describe('Exception viewer helpers', () => { value: undefined, }, { - fieldName: 'host.name.host.name', + fieldName: 'host.name', isNested: true, operator: 'is', value: 'some host name', }, { - fieldName: 'host.name.host.name', + fieldName: 'host.name', isNested: true, operator: 'is one of', value: ['some host name'], @@ -138,9 +138,9 @@ describe('Exception viewer helpers', () => { test('it formats as expected when "isNested" is "true"', () => { const payload = getEntryMatchMock(); - const formattedEntry = formatEntry({ isNested: true, parent: 'parent', item: payload }); + const formattedEntry = formatEntry({ isNested: true, item: payload }); const expected: FormattedEntry = { - fieldName: 'parent.host.name', + fieldName: 'host.name', isNested: true, operator: 'is', value: 'some host name', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx index 345db5bf1e75e..86b0512410e6f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx @@ -20,18 +20,16 @@ import * as i18n from '../translations'; */ export const formatEntry = ({ isNested, - parent, item, }: { isNested: boolean; - parent?: string; item: BuilderEntry; }): FormattedEntry => { const operator = getExceptionOperatorSelect(item); const value = getEntryValue(item); return { - fieldName: isNested ? `${parent}.${item.field}` : item.field ?? '', + fieldName: item.field ?? '', operator: operator.message, value, isNested, @@ -57,7 +55,6 @@ export const getFormattedEntries = (entries: BuilderEntry[]): FormattedEntry[] = (acc, nestedEntry) => { const formattedEntry = formatEntry({ isNested: true, - parent: item.field, item: nestedEntry, }); return [...acc, { ...formattedEntry }]; From 14355ab14b75e65ec3d597e2af54a3a93573385a Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Thu, 30 Jul 2020 17:44:03 -0400 Subject: [PATCH 13/55] [Security Solution][Telemetry] Concurrent telemetry requests (#73558) --- .../server/usage/collector.ts | 11 ++- .../usage/detections/detections_helpers.ts | 10 ++- .../server/usage/detections/index.ts | 24 ++++- .../server/usage/endpoints/endpoint.mocks.ts | 9 +- .../usage/endpoints/fleet_saved_objects.ts | 2 + .../server/usage/endpoints/index.ts | 90 +++++++++++-------- 6 files changed, 94 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 9a7ad6fc2db74..6fadf956ccaf1 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -6,7 +6,7 @@ import { LegacyAPICaller, CoreSetup } from '../../../../../src/core/server'; import { CollectorDependencies } from './types'; -import { DetectionsUsage, fetchDetectionsUsage } from './detections'; +import { DetectionsUsage, fetchDetectionsUsage, defaultDetectionsUsage } from './detections'; import { EndpointUsage, getEndpointTelemetryFromFleet } from './endpoints'; export type RegisterCollector = (deps: CollectorDependencies) => void; @@ -76,9 +76,14 @@ export const registerCollector: RegisterCollector = ({ isReady: () => kibanaIndex.length > 0, fetch: async (callCluster: LegacyAPICaller): Promise => { const savedObjectsClient = await getInternalSavedObjectsClient(core); + const [detections, endpoints] = await Promise.allSettled([ + fetchDetectionsUsage(kibanaIndex, callCluster, ml), + getEndpointTelemetryFromFleet(savedObjectsClient), + ]); + return { - detections: await fetchDetectionsUsage(kibanaIndex, callCluster, ml), - endpoints: await getEndpointTelemetryFromFleet(savedObjectsClient), + detections: detections.status === 'fulfilled' ? detections.value : defaultDetectionsUsage, + endpoints: endpoints.status === 'fulfilled' ? endpoints.value : {}, }; }, }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index f9905c373291c..80a9dba26df8e 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -23,7 +23,10 @@ interface DetectionsMetric { const isElasticRule = (tags: string[]) => tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`); -const initialRulesUsage: DetectionRulesUsage = { +/** + * Default detection rule usage count + */ +export const initialRulesUsage: DetectionRulesUsage = { custom: { enabled: 0, disabled: 0, @@ -34,7 +37,10 @@ const initialRulesUsage: DetectionRulesUsage = { }, }; -const initialMlJobsUsage: MlJobsUsage = { +/** + * Default ml job usage count + */ +export const initialMlJobsUsage: MlJobsUsage = { custom: { enabled: 0, disabled: 0, diff --git a/x-pack/plugins/security_solution/server/usage/detections/index.ts b/x-pack/plugins/security_solution/server/usage/detections/index.ts index dd50e79e22cc9..a366c86299b91 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/index.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/index.ts @@ -5,7 +5,12 @@ */ import { LegacyAPICaller } from '../../../../../../src/core/server'; -import { getMlJobsUsage, getRulesUsage } from './detections_helpers'; +import { + getMlJobsUsage, + getRulesUsage, + initialRulesUsage, + initialMlJobsUsage, +} from './detections_helpers'; import { MlPluginSetup } from '../../../../ml/server'; interface FeatureUsage { @@ -28,12 +33,23 @@ export interface DetectionsUsage { ml_jobs: MlJobsUsage; } +export const defaultDetectionsUsage = { + detection_rules: initialRulesUsage, + ml_jobs: initialMlJobsUsage, +}; + export const fetchDetectionsUsage = async ( kibanaIndex: string, callCluster: LegacyAPICaller, ml: MlPluginSetup | undefined ): Promise => { - const rulesUsage = await getRulesUsage(kibanaIndex, callCluster); - const mlJobsUsage = await getMlJobsUsage(ml); - return { detection_rules: rulesUsage, ml_jobs: mlJobsUsage }; + const [rulesUsage, mlJobsUsage] = await Promise.allSettled([ + getRulesUsage(kibanaIndex, callCluster), + getMlJobsUsage(ml), + ]); + + return { + detection_rules: rulesUsage.status === 'fulfilled' ? rulesUsage.value : initialRulesUsage, + ml_jobs: mlJobsUsage.status === 'fulfilled' ? mlJobsUsage.value : initialMlJobsUsage, + }; }; diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts index e3f0f7bde2fed..d753eeee93594 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts @@ -15,6 +15,7 @@ import { FLEET_ENDPOINT_PACKAGE_CONSTANT } from './fleet_saved_objects'; const testAgentId = 'testAgentId'; const testConfigId = 'testConfigId'; const testHostId = 'randoHostId'; +const testHostName = 'testDesktop'; /** Mock OS Platform for endpoint telemetry */ export const MockOSPlatform = 'somePlatform'; @@ -56,8 +57,8 @@ export const mockFleetObjectsResponse = ( }, }, host: { - hostname: 'testDesktop', - name: 'testDesktop', + hostname: testHostName, + name: testHostName, id: testHostId, }, os: { @@ -93,8 +94,8 @@ export const mockFleetObjectsResponse = ( }, }, host: { - hostname: 'testDesktop', - name: 'testDesktop', + hostname: hasDuplicates ? testHostName : 'oldRandoHostName', + name: hasDuplicates ? testHostName : 'oldRandoHostName', id: hasDuplicates ? testHostId : 'oldRandoHostId', }, os: { diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts index 42c1ec0e2eed2..c46610ec9388e 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts @@ -23,6 +23,8 @@ export const getFleetSavedObjectsMetadata = async (savedObjectsClient: ISavedObj 'last_checkin', 'local_metadata.agent.id', 'local_metadata.host.id', + 'local_metadata.host.name', + 'local_metadata.host.hostname', 'local_metadata.elastic.agent.id', 'local_metadata.os', ], diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts index 9e071f4adff25..19beda4554d93 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts @@ -42,7 +42,9 @@ export interface AgentLocalMetadata extends AgentMetadata { }; }; host: { + hostname: string; id: string; + name: string; }; os: { name: string; @@ -78,17 +80,20 @@ export const updateEndpointOSTelemetry = ( os: AgentLocalMetadata['os'], osTracker: OSTracker ): OSTracker => { - const updatedOSTracker = cloneDeep(osTracker); - const { version: osVersion, platform: osPlatform, full: osFullName } = os; - if (osFullName && osVersion) { - if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1; - else { - updatedOSTracker[osFullName] = { - full_name: osFullName, - platform: osPlatform, - version: osVersion, - count: 1, - }; + let updatedOSTracker = osTracker; + if (os && typeof os === 'object') { + updatedOSTracker = cloneDeep(osTracker); + const { version: osVersion, platform: osPlatform, full: osFullName } = os; + if (osFullName && osVersion) { + if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1; + else { + updatedOSTracker[osFullName] = { + full_name: osFullName, + platform: osPlatform, + version: osVersion, + count: 1, + }; + } } } @@ -211,46 +216,53 @@ export const getEndpointTelemetryFromFleet = async ( if (!endpointAgents || endpointAgentsCount < 1) return endpointTelemetry; // Use unique hosts to prevent any potential duplicates - const uniqueHostIds: Set = new Set(); + const uniqueHosts: Set = new Set(); let osTracker: OSTracker = {}; let dailyActiveCount = 0; let policyTracker: PoliciesTelemetry = { malware: { active: 0, inactive: 0, failure: 0 } }; for (let i = 0; i < endpointAgentsCount; i += 1) { - const { attributes: metadataAttributes } = endpointAgents[i]; - const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; - const { host, os, elastic } = localMetadata as AgentLocalMetadata; // AgentMetadata is just an empty blob, casting for our use case - - if (!uniqueHostIds.has(host.id)) { - uniqueHostIds.add(host.id); - const agentId = elastic?.agent?.id; - osTracker = updateEndpointOSTelemetry(os, osTracker); - - if (agentId) { - let agentEvents; - try { - const response = await getLatestFleetEndpointEvent(soClient, agentId); - agentEvents = response.saved_objects; - } catch (error) { - // If the request fails we do not obtain `active within last 24 hours for this agent` or policy specifics - } - - // AgentEvents will have a max length of 1 - if (agentEvents && agentEvents.length > 0) { - const latestEndpointEvent = agentEvents[0]; - dailyActiveCount = updateEndpointDailyActiveCount( - latestEndpointEvent, - lastCheckin, - dailyActiveCount + try { + const { attributes: metadataAttributes } = endpointAgents[i]; + const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; + const { host, os, elastic } = localMetadata as AgentLocalMetadata; + + // Although not perfect, the goal is to dedupe hosts to get the most recent data for a host + // An agent re-installed on the same host will have the same id and hostname + // A cloned VM will have the same id, but "may" have the same hostname, but it's really up to the user. + const compoundUniqueId = `${host?.id}-${host?.hostname}`; + if (!uniqueHosts.has(compoundUniqueId)) { + uniqueHosts.add(compoundUniqueId); + const agentId = elastic?.agent?.id; + osTracker = updateEndpointOSTelemetry(os, osTracker); + + if (agentId) { + const { saved_objects: agentEvents } = await getLatestFleetEndpointEvent( + soClient, + agentId ); - policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker); + + // AgentEvents will have a max length of 1 + if (agentEvents && agentEvents.length > 0) { + const latestEndpointEvent = agentEvents[0]; + dailyActiveCount = updateEndpointDailyActiveCount( + latestEndpointEvent, + lastCheckin, + dailyActiveCount + ); + policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker); + } } } + } catch (error) { + // All errors thrown in the loop would be handled here + // Not logging any errors to avoid leaking any potential PII + // Depending on when the error is thrown in the loop some specifics may be missing, but it allows the loop to continue } } // All unique hosts with an endpoint installed, thus all unique endpoint installs - endpointTelemetry.total_installed = uniqueHostIds.size; + endpointTelemetry.total_installed = uniqueHosts.size; // Set the daily active count for the endpoints endpointTelemetry.active_within_last_24_hours = dailyActiveCount; // Get the objects to populate our OS Telemetry From 7e2e78b54e71db6925d362dae0fca14c186b06d8 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 30 Jul 2020 15:51:01 -0600 Subject: [PATCH 14/55] [Maps] upgrade turf (#73816) * [Maps] upgrade turf * clean up ts-ignore comments * fix license check and review feedback Co-authored-by: Elastic Machine --- src/dev/license_checker/config.ts | 2 +- x-pack/package.json | 5 +- .../public/actions/data_request_actions.ts | 6 +- .../maps/public/actions/map_actions.ts | 10 +- .../es_pew_pew_source/es_pew_pew_source.js | 5 +- .../public/classes/util/can_skip_fetch.ts | 7 +- .../util/get_feature_collection_bounds.ts | 5 +- .../map/mb/draw_control/draw_circle.ts | 10 +- yarn.lock | 575 +----------------- 9 files changed, 47 insertions(+), 578 deletions(-) diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index efc42405688d4..60172a3106276 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -79,7 +79,7 @@ export const DEV_ONLY_LICENSE_WHITELIST = ['MPL-2.0']; // Globally overrides a license for a given package@version export const LICENSE_OVERRIDES = { - 'jsts@1.1.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts + 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint // TODO can be removed if the https://github.com/jindw/xmldom/issues/239 is released diff --git a/x-pack/package.json b/x-pack/package.json index 2d7cb148c43b0..e3104aabbb02b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -218,8 +218,12 @@ "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@scant/router": "^0.1.0", "@slack/webhook": "^5.0.0", + "@turf/bbox": "6.0.1", + "@turf/bbox-polygon": "6.0.1", "@turf/boolean-contains": "6.0.1", "@turf/circle": "6.0.1", + "@turf/distance": "6.0.1", + "@turf/helpers": "6.0.1", "angular": "^1.7.9", "angular-resource": "1.7.9", "angular-sanitize": "1.7.9", @@ -367,7 +371,6 @@ "tinymath": "1.2.1", "topojson-client": "3.0.0", "tslib": "^2.0.0", - "turf": "3.0.14", "typescript-fsa": "^3.0.0", "typescript-fsa-reducers": "^1.2.1", "ui-select": "0.19.8", diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index f91e272d625f6..4c829f8e75c20 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -6,8 +6,8 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { Dispatch } from 'redux'; -// @ts-ignore -import turf from 'turf'; +import bbox from '@turf/bbox'; +import { multiPoint } from '@turf/helpers'; import { FeatureCollection } from 'geojson'; import { MapStoreState } from '../reducers/store'; import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID } from '../../common/constants'; @@ -368,7 +368,7 @@ export function fitToDataBounds() { return; } - const dataBounds = turfBboxToBounds(turf.bbox(turf.multiPoint(corners))); + const dataBounds = turfBboxToBounds(bbox(multiPoint(corners))); dispatch(setGotoWithBounds(scaleBounds(dataBounds, FIT_TO_BOUNDS_SCALE_FACTOR))); }; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index f3619fd1bd23e..4914432f02de0 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -6,10 +6,10 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { Dispatch } from 'redux'; -// @ts-ignore -import turf from 'turf'; -import uuid from 'uuid/v4'; +import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; +import uuid from 'uuid/v4'; + import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { @@ -126,13 +126,13 @@ export function mapExtentChanged(newMapConstants: { zoom: number; extent: MapExt if (extent) { let doesBufferContainExtent = false; if (buffer) { - const bufferGeometry = turf.bboxPolygon([ + const bufferGeometry = turfBboxPolygon([ buffer.minLon, buffer.minLat, buffer.maxLon, buffer.maxLat, ]); - const extentGeometry = turf.bboxPolygon([ + const extentGeometry = turfBboxPolygon([ extent.minLon, extent.minLat, extent.maxLon, diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index 33d5deef2e39f..79eccf09b2888 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -6,7 +6,8 @@ import React from 'react'; import uuid from 'uuid/v4'; -import turf from 'turf'; +import turfBbox from '@turf/bbox'; +import { multiPoint } from '@turf/helpers'; import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; @@ -216,7 +217,7 @@ export class ESPewPewSource extends AbstractESAggSource { return null; } - return turfBboxToBounds(turf.bbox(turf.multiPoint(corners))); + return turfBboxToBounds(turfBbox(multiPoint(corners))); } canFormatFeatureProperties() { diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts index 8398bd7af39ad..147870dbef371 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import _ from 'lodash'; -// @ts-ignore -import turf from 'turf'; +import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; import { isRefreshOnlyQuery } from './is_refresh_only_query'; import { ISource } from '../sources/source'; @@ -27,13 +26,13 @@ export function updateDueToExtent(prevMeta: DataMeta = {}, nextMeta: DataMeta = return NO_SOURCE_UPDATE_REQUIRED; } - const previousBufferGeometry = turf.bboxPolygon([ + const previousBufferGeometry = turfBboxPolygon([ previousBuffer.minLon, previousBuffer.minLat, previousBuffer.maxLon, previousBuffer.maxLat, ]); - const newBufferGeometry = turf.bboxPolygon([ + const newBufferGeometry = turfBboxPolygon([ newBuffer.minLon, newBuffer.minLat, newBuffer.maxLon, diff --git a/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts index aa78d7064fb0a..76d305f0162d2 100644 --- a/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts +++ b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -import turf from 'turf'; +import turfBbox from '@turf/bbox'; import { FeatureCollection } from 'geojson'; import { MapExtent } from '../../../common/descriptor_types'; import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; @@ -28,7 +27,7 @@ export function getFeatureCollectionBounds( return null; } - const bbox = turf.bbox({ + const bbox = turfBbox({ type: 'FeatureCollection', features: visibleFeatures, }); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts index f2ceb8685d43e..3e89d67e11504 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts +++ b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts @@ -6,9 +6,9 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -// @ts-ignore -import turf from 'turf'; -// @ts-ignore +// @ts-expect-error +import turfDistance from '@turf/distance'; +// @ts-expect-error import turfCircle from '@turf/circle'; type DrawCircleState = { @@ -75,7 +75,7 @@ export const DrawCircle = { // second click, finish draw // @ts-ignore this.updateUIClasses({ mouse: 'pointer' }); - state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, [ + state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, [ e.lngLat.lng, e.lngLat.lat, ]); @@ -90,7 +90,7 @@ export const DrawCircle = { } const mouseLocation = [e.lngLat.lng, e.lngLat.lat]; - state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, mouseLocation); + state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, mouseLocation); const newCircleFeature = turfCircle( state.circle.properties.center, state.circle.properties.radiusKm diff --git a/yarn.lock b/yarn.lock index 60d073330b35d..4e9f732f1e0a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4092,7 +4092,14 @@ "@testing-library/dom" "^6.3.0" "@types/testing-library__react" "^9.1.0" -"@turf/bbox@6.x": +"@turf/bbox-polygon@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/bbox-polygon/-/bbox-polygon-6.0.1.tgz#ae0fbb14558831fb34538aae089a23d3336c6379" + integrity sha512-f6BK6GOzUNjmJeOYHklk/5LNcQMQbo51gvAM10dTM5IqzKP01KM5bgV88uOKfSZB0HRQVpaRV1tgXk2bg5cPRg== + dependencies: + "@turf/helpers" "6.x" + +"@turf/bbox@6.0.1", "@turf/bbox@6.x": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-6.0.1.tgz#b966075771475940ee1c16be2a12cf389e6e923a" integrity sha512-EGgaRLettBG25Iyx7VyUINsPpVj1x3nFQFiGS3ER8KCI1MximzNLsam3eXRabqQDjyAKyAE1bJ4EZEpGvspQxw== @@ -4143,6 +4150,19 @@ "@turf/helpers" "6.x" "@turf/invariant" "6.x" +"@turf/distance@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/distance/-/distance-6.0.1.tgz#0761f28784286e7865a427c4e7e3593569c2dea8" + integrity sha512-q7t7rWIWfkg7MP1Vt4uLjSEhe5rPfCO2JjpKmk7JC+QZKEQkuvHEqy3ejW1iC7Kw5ZcZNR3qdMGGz+6HnVwqvg== + dependencies: + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + +"@turf/helpers@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.0.1.tgz#625112616159e519033dc5d24c094ccbce7a457f" + integrity sha512-EQtcbwiNbkBnvxvlxcTcrwTzaq2eR4fnDVlzx/hsw5tYxDiMJfuL9goy1rDCmXxcyDnk8gCyZapYfEQWYLl1pQ== + "@turf/helpers@6.x": version "6.1.4" resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.1.4.tgz#d6fd7ebe6782dd9c87dca5559bda5c48ae4c3836" @@ -6229,13 +6249,6 @@ adm-zip@0.4.11: resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.11.tgz#2aa54c84c4b01a9d0fb89bb11982a51f13e3d62a" integrity sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA== -affine-hull@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/affine-hull/-/affine-hull-1.0.0.tgz#763ff1d38d063ceb7e272f17ee4d7bbcaf905c5d" - integrity sha1-dj/x040GPOt+Jy8X7k17vK+QXF0= - dependencies: - robust-orientation "^1.1.3" - after-all-results@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/after-all-results/-/after-all-results-2.0.0.tgz#6ac2fc202b500f88da8f4f5530cfa100f4c6a2d0" @@ -8216,11 +8229,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bit-twiddle@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz#0c6c1fabe2b23d17173d9a61b7b7093eb9e1769e" - integrity sha1-DGwfq+KyPRcXPZpht7cJPrnhdp4= - bl@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" @@ -10374,15 +10382,6 @@ convert-source-map@^0.3.3: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= -convex-hull@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/convex-hull/-/convex-hull-1.0.3.tgz#20a3aa6ce87f4adea2ff7d17971c9fc1c67e1fff" - integrity sha1-IKOqbOh/St6i/30XlxyfwcZ+H/8= - dependencies: - affine-hull "^1.0.0" - incremental-convex-hull "^1.0.1" - monotone-convex-hull-2d "^1.0.1" - cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -12300,11 +12299,6 @@ eachr@^3.2.0: editions "^1.1.1" typechecker "^4.3.0" -earcut@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.1.3.tgz#ca579545f351941af7c3d0df49c9f7d34af99b0c" - integrity sha512-AxdCdWUk1zzK/NuZ7e1ljj6IGC+VAdC3Qb7QQDsXpfNrc5IM8tL9nNXUmEGE6jRHTfZ10zhzRhtDmWVsR5pd3A== - earcut@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.2.tgz#41b0bc35f63e0fe80da7cddff28511e7e2e80d11" @@ -14897,13 +14891,6 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -geojson-area@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/geojson-area/-/geojson-area-0.2.1.tgz#2537b0982db86309f21d2c428a4257c7a6282cc6" - integrity sha1-JTewmC24YwnyHSxCikJXx6YoLMY= - dependencies: - wgs84 "0.0.0" - geojson-flatten@~0.2.1: version "0.2.4" resolved "https://registry.yarnpkg.com/geojson-flatten/-/geojson-flatten-0.2.4.tgz#8f3396f31a0f5b747e39c9e6a14088f43ba4ecfb" @@ -14912,16 +14899,6 @@ geojson-flatten@~0.2.1: get-stdin "^6.0.0" minimist "1.2.0" -geojson-normalize@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/geojson-normalize/-/geojson-normalize-0.0.0.tgz#2dbc3678cd1b31b8179e876bda70cd120dde35c0" - integrity sha1-Lbw2eM0bMbgXnodr2nDNEg3eNcA= - -geojson-random@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/geojson-random/-/geojson-random-0.2.2.tgz#ab4838f126adc5e16f8f94e655def820f9119dbc" - integrity sha1-q0g48SatxeFvj5TmVd74IPkRnbw= - geojson-vt@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7" @@ -17083,14 +17060,6 @@ in-publish@^2.0.0: resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= -incremental-convex-hull@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/incremental-convex-hull/-/incremental-convex-hull-1.0.1.tgz#51428c14cb9d9a6144bfe69b2851fb377334be1e" - integrity sha1-UUKMFMudmmFEv+abKFH7N3M0vh4= - dependencies: - robust-orientation "^1.1.2" - simplicial-complex "^1.0.0" - indent-string@3.2.0, indent-string@^3.0.0, indent-string@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" @@ -19410,11 +19379,6 @@ jstransformer@1.0.0, jstransformer@^1.0.0: is-promise "^2.0.0" promise "^7.0.1" -jsts@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/jsts/-/jsts-1.1.2.tgz#d205d2cc8393081d9e484ae36282110695edc230" - integrity sha1-0gXSzIOTCB2eSErjYoIRBpXtwjA= - jsts@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/jsts/-/jsts-1.6.2.tgz#c0efc885edae06ae84f78cbf2a0110ba929c5925" @@ -21560,13 +21524,6 @@ monocle-ts@^1.0.0: resolved "https://registry.yarnpkg.com/monocle-ts/-/monocle-ts-1.7.1.tgz#03a615938aa90983a4fa29749969d30f72d80ba1" integrity sha512-X9OzpOyd/R83sYex8NYpJjUzi/MLQMvGNVfxDYiIvs+QMXMEUDwR61MQoARFN10Cqz5h/mbFSPnIQNUIGhYd2Q== -monotone-convex-hull-2d@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz#47f5daeadf3c4afd37764baa1aa8787a40eee08c" - integrity sha1-R/Xa6t88Sv03dkuqGqh4ekDu4Iw= - dependencies: - robust-orientation "^1.1.3" - moo@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" @@ -26869,34 +26826,6 @@ rison-node@1.0.2: resolved "https://registry.yarnpkg.com/rison-node/-/rison-node-1.0.2.tgz#b7b5f37f39f5ae2a51a973a33c9aa17239a33e4b" integrity sha1-t7Xzfzn1ripRqXOjPJqhcjmjPks= -robust-orientation@^1.1.2, robust-orientation@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/robust-orientation/-/robust-orientation-1.1.3.tgz#daff5b00d3be4e60722f0e9c0156ef967f1c2049" - integrity sha1-2v9bANO+TmByLw6cAVbvln8cIEk= - dependencies: - robust-scale "^1.0.2" - robust-subtract "^1.0.0" - robust-sum "^1.0.0" - two-product "^1.0.2" - -robust-scale@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/robust-scale/-/robust-scale-1.0.2.tgz#775132ed09542d028e58b2cc79c06290bcf78c32" - integrity sha1-d1Ey7QlULQKOWLLMecBikLz3jDI= - dependencies: - two-product "^1.0.2" - two-sum "^1.0.0" - -robust-subtract@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/robust-subtract/-/robust-subtract-1.0.0.tgz#e0b164e1ed8ba4e3a5dda45a12038348dbed3e9a" - integrity sha1-4LFk4e2LpOOl3aRaEgODSNvtPpo= - -robust-sum@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/robust-sum/-/robust-sum-1.0.0.tgz#16646e525292b4d25d82757a286955e0bbfa53d9" - integrity sha1-FmRuUlKStNJdgnV6KGlV4Lv6U9k= - rollup@^0.25.8: version "0.25.8" resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.25.8.tgz#bf6ce83b87510d163446eeaa577ed6a6fc5835e0" @@ -27757,19 +27686,6 @@ simplebar@^4.2.0: lodash.throttle "^4.1.1" resize-observer-polyfill "^1.5.1" -simplicial-complex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/simplicial-complex/-/simplicial-complex-1.0.0.tgz#6c33a4ed69fcd4d91b7bcadd3b30b63683eae241" - integrity sha1-bDOk7Wn81Nkbe8rdOzC2NoPq4kE= - dependencies: - bit-twiddle "^1.0.0" - union-find "^1.0.0" - -simplify-js@^1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/simplify-js/-/simplify-js-1.2.3.tgz#a3422c1b9884d60421345eb44d2b872662df27f5" - integrity sha512-0IkEqs+5c5vROkHaifGfbqHf5tYDcsTBy6oJPRbFCSwp2uzEr+PpH3dNP7wD8O3d7zdUCjLVq1/xHkwA/JjlFA== - sinon@^7.4.2: version "7.5.0" resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.5.0.tgz#e9488ea466070ea908fd44a3d6478fd4923c67ec" @@ -30027,440 +29943,6 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== -turf-along@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-along/-/turf-along-3.0.12.tgz#e622bde7a4bd138c09647d4b14aa0ea700485de6" - integrity sha1-5iK956S9E4wJZH1LFKoOpwBIXeY= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-area@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-area/-/turf-area-3.0.12.tgz#9b7e469ef9fb558fd147bb0c214823263bdbf13c" - integrity sha1-m35Gnvn7VY/RR7sMIUgjJjvb8Tw= - dependencies: - geojson-area "^0.2.1" - -turf-bbox-polygon@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bbox-polygon/-/turf-bbox-polygon-3.0.12.tgz#330dc0bb38322d61545df966ce6c80f685acf4f2" - integrity sha1-Mw3AuzgyLWFUXflmzmyA9oWs9PI= - dependencies: - turf-helpers "^3.0.12" - -turf-bbox@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bbox/-/turf-bbox-3.0.12.tgz#3fa06117c8443860ec80ac60fd5d2f1320bfb1be" - integrity sha1-P6BhF8hEOGDsgKxg/V0vEyC/sb4= - dependencies: - turf-meta "^3.0.12" - -turf-bearing@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bearing/-/turf-bearing-3.0.12.tgz#65f609dd850e7364c7771aa6ded87b0e1917fd20" - integrity sha1-ZfYJ3YUOc2THdxqm3th7DhkX/SA= - dependencies: - turf-invariant "^3.0.12" - -turf-bezier@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bezier/-/turf-bezier-3.0.12.tgz#102efdd4a63b265ee9c8c1727631920b36f4dd02" - integrity sha1-EC791KY7Jl7pyMFydjGSCzb03QI= - dependencies: - turf-helpers "^3.0.12" - -turf-buffer@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-buffer/-/turf-buffer-3.0.12.tgz#20840fe7c6aa67b24be1cab7ffcc5a82fd6bd971" - integrity sha1-IIQP58aqZ7JL4cq3/8xagv1r2XE= - dependencies: - geojson-normalize "0.0.0" - jsts "1.1.2" - turf-combine "^3.0.12" - turf-helpers "^3.0.12" - -turf-center@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-center/-/turf-center-3.0.12.tgz#45dd6c1729bb867291e3e002e9c7506f8c440196" - integrity sha1-Rd1sFym7hnKR4+AC6cdQb4xEAZY= - dependencies: - turf-bbox "^3.0.12" - turf-helpers "^3.0.12" - -turf-centroid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-centroid/-/turf-centroid-3.0.12.tgz#eaee0d698204b57fc33994bb1bc867b8da293f8f" - integrity sha1-6u4NaYIEtX/DOZS7G8hnuNopP48= - dependencies: - turf-helpers "^3.0.12" - turf-meta "^3.0.12" - -turf-circle@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-circle/-/turf-circle-3.0.12.tgz#140b21cb4950f2d3cbc70d2df012936867f58930" - integrity sha1-FAshy0lQ8tPLxw0t8BKTaGf1iTA= - dependencies: - turf-destination "^3.0.12" - turf-helpers "^3.0.12" - -turf-collect@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-collect/-/turf-collect-3.0.12.tgz#6e986d1a707da319cc83e7238d0bcdf19aa3c7f2" - integrity sha1-bphtGnB9oxnMg+cjjQvN8Zqjx/I= - dependencies: - turf-inside "^3.0.12" - -turf-combine@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-combine/-/turf-combine-3.0.12.tgz#1670746f0fdce0d1ea8aa6a29ffe5438d446cf73" - integrity sha1-FnB0bw/c4NHqiqain/5UONRGz3M= - dependencies: - turf-meta "^3.0.12" - -turf-concave@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-concave/-/turf-concave-3.0.12.tgz#fcab6056965b0a8319f6cd802601095f2fd3a8eb" - integrity sha1-/KtgVpZbCoMZ9s2AJgEJXy/TqOs= - dependencies: - turf-distance "^3.0.12" - turf-meta "^3.0.12" - turf-tin "^3.0.12" - turf-union "^3.0.12" - -turf-convex@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-convex/-/turf-convex-3.0.12.tgz#a88ddc3e22d1cb658796a9c85d3ada3bd3eca357" - integrity sha1-qI3cPiLRy2WHlqnIXTraO9Pso1c= - dependencies: - convex-hull "^1.0.3" - turf-helpers "^3.0.12" - turf-meta "^3.0.12" - -turf-destination@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-destination/-/turf-destination-3.0.12.tgz#7dd6fbf97e86f831a26c83ef2d5a2f8d1d8a6de2" - integrity sha1-fdb7+X6G+DGibIPvLVovjR2KbeI= - dependencies: - turf-helpers "^3.0.12" - turf-invariant "^3.0.12" - -turf-difference@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-difference/-/turf-difference-3.0.12.tgz#9c3d0d7630421005b8b25b7f068ed9efb4bc6ea7" - integrity sha1-nD0NdjBCEAW4slt/Bo7Z77S8bqc= - dependencies: - jsts "1.1.2" - turf-helpers "^3.0.12" - -turf-distance@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-distance/-/turf-distance-3.0.12.tgz#fb97b8705facd993b145e014b41862610eeca449" - integrity sha1-+5e4cF+s2ZOxReAUtBhiYQ7spEk= - dependencies: - turf-helpers "^3.0.12" - turf-invariant "^3.0.12" - -turf-envelope@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-envelope/-/turf-envelope-3.0.12.tgz#96921d278cc8c664692e320e2543b914080d786b" - integrity sha1-lpIdJ4zIxmRpLjIOJUO5FAgNeGs= - dependencies: - turf-bbox "^3.0.12" - turf-bbox-polygon "^3.0.12" - -turf-explode@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-explode/-/turf-explode-3.0.12.tgz#c5ae28c284cd006c56511ec7d408c48a5414ecfe" - integrity sha1-xa4owoTNAGxWUR7H1AjEilQU7P4= - dependencies: - turf-helpers "^3.0.12" - turf-meta "^3.0.12" - -turf-flip@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-flip/-/turf-flip-3.0.12.tgz#deb868177b9ff3bb310c5d41aaac61a9156a3cbb" - integrity sha1-3rhoF3uf87sxDF1BqqxhqRVqPLs= - dependencies: - turf-meta "^3.0.12" - -turf-grid@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/turf-grid/-/turf-grid-1.0.1.tgz#b904abc564b939b627a66ac15eb16e053829b80f" - integrity sha1-uQSrxWS5ObYnpmrBXrFuBTgpuA8= - dependencies: - turf-point "^2.0.0" - -turf-helpers@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-helpers/-/turf-helpers-3.0.12.tgz#dd4272e74b3ad7c96eecb7ae5c57fe8eca544b7b" - integrity sha1-3UJy50s618lu7LeuXFf+jspUS3s= - -turf-hex-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-hex-grid/-/turf-hex-grid-3.0.12.tgz#0698ef669020bb31d8e9cc2056d0abfcafc84e8f" - integrity sha1-BpjvZpAguzHY6cwgVtCr/K/ITo8= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-inside@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-inside/-/turf-inside-3.0.12.tgz#9ba40fa6eed63bec7e7d88aa6427622c4df07066" - integrity sha1-m6QPpu7WO+x+fYiqZCdiLE3wcGY= - dependencies: - turf-invariant "^3.0.12" - -turf-intersect@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-intersect/-/turf-intersect-3.0.12.tgz#c0d7fb305843a19275670057a39d268b17830d83" - integrity sha1-wNf7MFhDoZJ1ZwBXo50mixeDDYM= - dependencies: - jsts "1.1.2" - -turf-invariant@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-invariant/-/turf-invariant-3.0.12.tgz#3b95253953991ebd962dd35d4f6704c287de8ebe" - integrity sha1-O5UlOVOZHr2WLdNdT2cEwofejr4= - -turf-isolines@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-isolines/-/turf-isolines-3.0.12.tgz#00b233dfe2eebd4ecb47a94fc923c6ecec89c7ab" - integrity sha1-ALIz3+LuvU7LR6lPySPG7OyJx6s= - dependencies: - turf-bbox "^3.0.12" - turf-grid "1.0.1" - turf-helpers "^3.0.12" - turf-inside "^3.0.12" - turf-planepoint "^3.0.12" - turf-square "^3.0.12" - turf-tin "^3.0.12" - -turf-kinks@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-kinks/-/turf-kinks-3.0.12.tgz#e9c9a8dba5724d98f2350fc5bdeba069ec333755" - integrity sha1-6cmo26VyTZjyNQ/FveugaewzN1U= - dependencies: - turf-helpers "^3.0.12" - -turf-line-distance@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-line-distance/-/turf-line-distance-3.0.12.tgz#7108f5b26907f7b8c2dd1b3997866dd3a60e8f5f" - integrity sha1-cQj1smkH97jC3Rs5l4Zt06YOj18= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-line-slice@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-line-slice/-/turf-line-slice-3.0.12.tgz#f5f1accc92adae69ea1ac0b29f07529a28dde916" - integrity sha1-9fGszJKtrmnqGsCynwdSmijd6RY= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - turf-point-on-line "^3.0.12" - -turf-meta@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-meta/-/turf-meta-3.0.12.tgz#0aa9a1caf82b2a5a08d54e0830b5b5a3fa0e8a38" - integrity sha1-CqmhyvgrKloI1U4IMLW1o/oOijg= - -turf-midpoint@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-midpoint/-/turf-midpoint-3.0.12.tgz#b12765ae89acdee8556fd5e26c9c5fa041a02cbe" - integrity sha1-sSdlroms3uhVb9XibJxfoEGgLL4= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-invariant "^3.0.12" - -turf-nearest@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-nearest/-/turf-nearest-3.0.12.tgz#700207f4443f05096f86cd246f929f170dfaf46d" - integrity sha1-cAIH9EQ/BQlvhs0kb5KfFw369G0= - dependencies: - turf-distance "^3.0.12" - -turf-planepoint@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-planepoint/-/turf-planepoint-3.0.12.tgz#2c37ae0f17fcb30db6e38f0d59ee6c0dd6caa9af" - integrity sha1-LDeuDxf8sw22448NWe5sDdbKqa8= - -turf-point-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-point-grid/-/turf-point-grid-3.0.12.tgz#d604978be10bc9e53306ae02cef7098431db4971" - integrity sha1-1gSXi+ELyeUzBq4CzvcJhDHbSXE= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-point-on-line@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-point-on-line/-/turf-point-on-line-3.0.12.tgz#1d8663354e70372db1863e6253e9040c47127b0f" - integrity sha1-HYZjNU5wNy2xhj5iU+kEDEcSew8= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-point-on-surface@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-point-on-surface/-/turf-point-on-surface-3.0.12.tgz#9be505b6b0ba78e98565001de3b3a4267115240a" - integrity sha1-m+UFtrC6eOmFZQAd47OkJnEVJAo= - dependencies: - turf-center "^3.0.12" - turf-distance "^3.0.12" - turf-explode "^3.0.12" - turf-helpers "^3.0.12" - turf-inside "^3.0.12" - -turf-point@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/turf-point/-/turf-point-2.0.1.tgz#a2dcc30a2d20f44cf5c6271df7bae2c0e2146069" - integrity sha1-otzDCi0g9Ez1xicd97riwOIUYGk= - dependencies: - minimist "^1.1.0" - -turf-random@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-random/-/turf-random-3.0.12.tgz#34dbb141c3f1eaeae1424fd6c5eaba1f6fb9b1e8" - integrity sha1-NNuxQcPx6urhQk/Wxeq6H2+5seg= - dependencies: - geojson-random "^0.2.2" - -turf-sample@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-sample/-/turf-sample-3.0.12.tgz#7949f8620612047e1314c1ced87e99c142463cd2" - integrity sha1-eUn4YgYSBH4TFMHO2H6ZwUJGPNI= - dependencies: - turf-helpers "^3.0.12" - -turf-simplify@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-simplify/-/turf-simplify-3.0.12.tgz#85e443c8b46aa2b7526389444c7381daa2ad19e7" - integrity sha1-heRDyLRqordSY4lETHOB2qKtGec= - dependencies: - simplify-js "^1.2.1" - -turf-square-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-square-grid/-/turf-square-grid-3.0.12.tgz#3c1d80ac14556c6813b478bda012512ed4b93ec8" - integrity sha1-PB2ArBRVbGgTtHi9oBJRLtS5Psg= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-square@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-square/-/turf-square-3.0.12.tgz#1a38b1e0fb05ffe0fcaa43188e2f37942a515b64" - integrity sha1-Gjix4PsF/+D8qkMYji83lCpRW2Q= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-tag@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-tag/-/turf-tag-3.0.12.tgz#2284fff0e8a1e92a27d4ac7fd7471b3c48ddd1a8" - integrity sha1-IoT/8Oih6Son1Kx/10cbPEjd0ag= - dependencies: - turf-inside "^3.0.12" - -turf-tesselate@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-tesselate/-/turf-tesselate-3.0.12.tgz#41474b7b5b3820bcf273fb71e1894d8c3cd40d35" - integrity sha1-QUdLe1s4ILzyc/tx4YlNjDzUDTU= - dependencies: - earcut "^2.0.0" - turf-helpers "^3.0.12" - -turf-tin@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-tin/-/turf-tin-3.0.12.tgz#b6534644763ace1c9df241c958d2384855257385" - integrity sha1-tlNGRHY6zhyd8kHJWNI4SFUlc4U= - dependencies: - turf-helpers "^3.0.12" - -turf-triangle-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-triangle-grid/-/turf-triangle-grid-3.0.12.tgz#80647e57dafe09346879a29a18a0e6294acf1159" - integrity sha1-gGR+V9r+CTRoeaKaGKDmKUrPEVk= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-union@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-union/-/turf-union-3.0.12.tgz#dfed0e5540b8c2855e4994c14621e3a60c829c8e" - integrity sha1-3+0OVUC4woVeSZTBRiHjpgyCnI4= - dependencies: - jsts "1.1.2" - -turf-within@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-within/-/turf-within-3.0.12.tgz#f77eeaf377238561b7fb1338e76e9d1298741f94" - integrity sha1-937q83cjhWG3+xM4526dEph0H5Q= - dependencies: - turf-helpers "^3.0.12" - turf-inside "^3.0.12" - -turf@3.0.14: - version "3.0.14" - resolved "https://registry.yarnpkg.com/turf/-/turf-3.0.14.tgz#eb2f4a80a2d583b8c6486bc7b5c7190466866c27" - integrity sha1-6y9KgKLVg7jGSGvHtccZBGaGbCc= - dependencies: - turf-along "^3.0.12" - turf-area "^3.0.12" - turf-bbox "^3.0.12" - turf-bbox-polygon "^3.0.12" - turf-bearing "^3.0.12" - turf-bezier "^3.0.12" - turf-buffer "^3.0.12" - turf-center "^3.0.12" - turf-centroid "^3.0.12" - turf-circle "^3.0.12" - turf-collect "^3.0.12" - turf-combine "^3.0.12" - turf-concave "^3.0.12" - turf-convex "^3.0.12" - turf-destination "^3.0.12" - turf-difference "^3.0.12" - turf-distance "^3.0.12" - turf-envelope "^3.0.12" - turf-explode "^3.0.12" - turf-flip "^3.0.12" - turf-helpers "^3.0.12" - turf-hex-grid "^3.0.12" - turf-inside "^3.0.12" - turf-intersect "^3.0.12" - turf-isolines "^3.0.12" - turf-kinks "^3.0.12" - turf-line-distance "^3.0.12" - turf-line-slice "^3.0.12" - turf-meta "^3.0.12" - turf-midpoint "^3.0.12" - turf-nearest "^3.0.12" - turf-planepoint "^3.0.12" - turf-point-grid "^3.0.12" - turf-point-on-line "^3.0.12" - turf-point-on-surface "^3.0.12" - turf-random "^3.0.12" - turf-sample "^3.0.12" - turf-simplify "^3.0.12" - turf-square "^3.0.12" - turf-square-grid "^3.0.12" - turf-tag "^3.0.12" - turf-tesselate "^3.0.12" - turf-tin "^3.0.12" - turf-triangle-grid "^3.0.12" - turf-union "^3.0.12" - turf-within "^3.0.12" - tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -30475,16 +29957,6 @@ twig@^1.10.5: minimatch "3.0.x" walk "2.3.x" -two-product@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/two-product/-/two-product-1.0.2.tgz#67d95d4b257a921e2cb4bd7af9511f9088522eaa" - integrity sha1-Z9ldSyV6kh4stL16+VEfkIhSLqo= - -two-sum@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/two-sum/-/two-sum-1.0.0.tgz#31d3f32239e4f731eca9df9155e2b297f008ab64" - integrity sha1-MdPzIjnk9zHsqd+RVeKyl/AIq2Q= - type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -30839,11 +30311,6 @@ unified@^7.1.0: vfile "^3.0.0" x-is-string "^0.1.0" -union-find@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/union-find/-/union-find-1.0.2.tgz#292bac415e6ad3a89535d237010db4a536284e58" - integrity sha1-KSusQV5q06iVNdI3AQ20pTYoTlg= - union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" From 18795b2bf3247a9b1cd9e908ddaef3db374fd32f Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 30 Jul 2020 19:10:26 -0400 Subject: [PATCH 15/55] [Canvas][tech-debt] Fix SVG not shrinking vertically properly (#73867) Co-authored-by: Elastic Machine --- .../plugins/canvas/canvas_plugin_src/renderers/shape/index.js | 1 + .../canvas/public/components/render_with_fn/render_with_fn.tsx | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js index 02c86afd7182b..5684c8c4602b5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js @@ -75,6 +75,7 @@ export const shape = () => ({ domNode.removeChild(oldShape); } + domNode.style.lineHeight = 0; domNode.appendChild(shapeSvg); }; diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx index 7939c1d04631a..c5fe7074fea0b 100644 --- a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx +++ b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx @@ -5,7 +5,6 @@ */ import React, { useState, useEffect, useRef, FC, useCallback } from 'react'; -import { useDebounce } from 'react-use'; import { useNotifyService } from '../../services'; import { RenderToDom } from '../render_to_dom'; @@ -73,7 +72,7 @@ export const RenderWithFn: FC = ({ firstRender.current = true; }, [domNode]); - useDebounce(() => handlers.current.resize({ height, width }), 150, [height, width]); + useEffect(() => handlers.current.resize({ height, width }), [height, width]); useEffect( () => () => { From 9c9080c11e869c2452fe938af100105f3ee05924 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 30 Jul 2020 19:15:26 -0400 Subject: [PATCH 16/55] [Ingest Management] main branch uses epr-snapshot. Others production (#73555) * Same behavior as now. Just refactored. * main branch uses epr-snapshot. Others use prod * Link some types vs repeating them * replace DEFAULT_REGISTRY_URL with getRegistryUrl in Endpoint tests * Make an Endpoint test helper name more clear * try/catch around getKibanaBranch * Use branch & version from package.json as fallback * No guards b/c kibana{Branch,Version} have defaults Co-authored-by: Elastic Machine --- .../ingest_manager/common/constants/epm.ts | 1 - .../ingest_manager/server/constants/index.ts | 1 - x-pack/plugins/ingest_manager/server/index.ts | 2 +- .../plugins/ingest_manager/server/plugin.ts | 12 +++---- .../server/services/app_context.ts | 13 +++---- .../services/epm/registry/registry_url.ts | 35 ++++++++++++++----- .../ingest_manager/server/services/index.ts | 2 ++ .../apps/endpoint/index.ts | 6 ++-- .../apis/index.ts | 6 ++-- .../registry.ts | 6 ++-- 10 files changed, 48 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/constants/epm.ts b/x-pack/plugins/ingest_manager/common/constants/epm.ts index 3d3c91a4310f8..73cd8463bb6aa 100644 --- a/x-pack/plugins/ingest_manager/common/constants/epm.ts +++ b/x-pack/plugins/ingest_manager/common/constants/epm.ts @@ -6,5 +6,4 @@ export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages'; export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; -export const DEFAULT_REGISTRY_URL = 'https://epr-snapshot.ea-web.elastic.dev'; export const INDEX_PATTERN_PLACEHOLDER_SUFFIX = '-index_pattern_placeholder'; diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index ce81736f2e84f..1ec13bd80f0fb 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -43,5 +43,4 @@ export { // Defaults DEFAULT_AGENT_CONFIG, DEFAULT_OUTPUT, - DEFAULT_REGISTRY_URL, } from '../../common'; diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 40e0153a26581..6f8c4948559d3 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -6,7 +6,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { IngestManagerPlugin } from './plugin'; -export { AgentService, ESIndexPatternService } from './services'; +export { AgentService, ESIndexPatternService, getRegistryUrl } from './services'; export { IngestManagerSetupContract, IngestManagerSetupDeps, diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index e7495df254a09..e5e1194d59ecb 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -83,9 +83,9 @@ export interface IngestManagerAppContext { security?: SecurityPluginSetup; config$?: Observable; savedObjects: SavedObjectsServiceStart; - isProductionMode: boolean; - kibanaVersion: string; - kibanaBranch: string; + isProductionMode: PluginInitializerContext['env']['mode']['prod']; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + kibanaBranch: PluginInitializerContext['env']['packageInfo']['branch']; cloud?: CloudSetup; logger?: Logger; httpSetup?: HttpServiceSetup; @@ -144,9 +144,9 @@ export class IngestManagerPlugin private cloud: CloudSetup | undefined; private logger: Logger | undefined; - private isProductionMode: boolean; - private kibanaVersion: string; - private kibanaBranch: string; + private isProductionMode: IngestManagerAppContext['isProductionMode']; + private kibanaVersion: IngestManagerAppContext['kibanaVersion']; + private kibanaBranch: IngestManagerAppContext['kibanaBranch']; private httpSetup: HttpServiceSetup | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index bdc7a443ba6dd..7f82670a4d02c 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -10,6 +10,7 @@ import { EncryptedSavedObjectsClient, EncryptedSavedObjectsPluginSetup, } from '../../../encrypted_saved_objects/server'; +import packageJSON from '../../../../../package.json'; import { SecurityPluginSetup } from '../../../security/server'; import { IngestManagerConfigType } from '../../common'; import { ExternalCallback, ExternalCallbacksStorage, IngestManagerAppContext } from '../plugin'; @@ -22,9 +23,9 @@ class AppContextService { private config$?: Observable; private configSubject$?: BehaviorSubject; private savedObjects: SavedObjectsServiceStart | undefined; - private isProductionMode: boolean = false; - private kibanaVersion: string | undefined; - private kibanaBranch: string | undefined; + private isProductionMode: IngestManagerAppContext['isProductionMode'] = false; + private kibanaVersion: IngestManagerAppContext['kibanaVersion'] = packageJSON.version; + private kibanaBranch: IngestManagerAppContext['kibanaBranch'] = packageJSON.branch; private cloud?: CloudSetup; private logger: Logger | undefined; private httpSetup?: HttpServiceSetup; @@ -121,16 +122,10 @@ class AppContextService { } public getKibanaVersion() { - if (!this.kibanaVersion) { - throw new Error('Kibana version is not set.'); - } return this.kibanaVersion; } public getKibanaBranch() { - if (!this.kibanaBranch) { - throw new Error('Kibana branch is not set.'); - } return this.kibanaBranch; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts index 47c9121808988..b788d1bcbb4a9 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts @@ -3,20 +3,37 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_REGISTRY_URL } from '../../../constants'; import { appContextService, licenseService } from '../../'; +// from https://github.com/elastic/package-registry#docker (maybe from OpenAPI one day) +// the unused variables cause a TS warning about unused values +// chose to comment them out vs @ts-ignore or @ts-expect-error on each line + +const PRODUCTION_REGISTRY_URL_CDN = 'https://epr.elastic.co'; +// const STAGING_REGISTRY_URL_CDN = 'https://epr-staging.elastic.co'; +// const EXPERIMENTAL_REGISTRY_URL_CDN = 'https://epr-experimental.elastic.co/'; +const SNAPSHOT_REGISTRY_URL_CDN = 'https://epr-snapshot.elastic.co'; + +// const PRODUCTION_REGISTRY_URL_NO_CDN = 'https://epr.ea-web.elastic.dev'; +// const STAGING_REGISTRY_URL_NO_CDN = 'https://epr-staging.ea-web.elastic.dev'; +// const EXPERIMENTAL_REGISTRY_URL_NO_CDN = 'https://epr-experimental.ea-web.elastic.dev/'; +// const SNAPSHOT_REGISTRY_URL_NO_CDN = 'https://epr-snapshot.ea-web.elastic.dev'; + +const getDefaultRegistryUrl = (): string => { + const branch = appContextService.getKibanaBranch(); + if (branch === 'master') { + return SNAPSHOT_REGISTRY_URL_CDN; + } else { + return PRODUCTION_REGISTRY_URL_CDN; + } +}; + export const getRegistryUrl = (): string => { const license = licenseService.getLicenseInformation(); const customUrl = appContextService.getConfig()?.registryUrl; + const isGoldPlus = license?.isAvailable && license?.isActive && license?.hasAtLeast('gold'); - if ( - customUrl && - license && - license.isAvailable && - license.hasAtLeast('gold') && - license.isActive - ) { + if (customUrl && isGoldPlus) { return customUrl; } @@ -24,5 +41,5 @@ export const getRegistryUrl = (): string => { appContextService.getLogger().warn('Gold license is required to use a custom registry url.'); } - return DEFAULT_REGISTRY_URL; + return getDefaultRegistryUrl(); }; diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index 74adab09d12eb..f6ca9e7bbbe71 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -9,6 +9,8 @@ import { AgentStatus, Agent } from '../types'; import * as settingsService from './settings'; export { ESIndexPatternSavedObjectService } from './es_index_pattern'; +export { getRegistryUrl } from './epm/registry/registry_url'; + /** * Service to return the index pattern of EPM packages */ diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index 7962ec60ff57e..ad1980cd7218b 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -3,11 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_REGISTRY_URL } from '../../../../plugins/ingest_manager/common'; +import { getRegistryUrl as getRegistryUrlFromIngest } from '../../../../plugins/ingest_manager/server'; import { FtrProviderContext } from '../../ftr_provider_context'; import { isRegistryEnabled, - getRegistryUrl, + getRegistryUrlFromTestEnv, } from '../../../security_solution_endpoint_api_int/registry'; export default function (providerContext: FtrProviderContext) { @@ -22,7 +22,7 @@ export default function (providerContext: FtrProviderContext) { log.warning('These tests are being run with an external package registry'); } - const registryUrl = getRegistryUrl() ?? DEFAULT_REGISTRY_URL; + const registryUrl = getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); log.info(`Package registry URL for tests: ${registryUrl}`); before(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index b1317c2d9f1c1..9cdef1c938890 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { FtrProviderContext } from '../ftr_provider_context'; -import { isRegistryEnabled, getRegistryUrl } from '../registry'; -import { DEFAULT_REGISTRY_URL } from '../../../plugins/ingest_manager/common'; +import { isRegistryEnabled, getRegistryUrlFromTestEnv } from '../registry'; +import { getRegistryUrl as getRegistryUrlFromIngest } from '../../../plugins/ingest_manager/server'; export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { const { loadTestFile, getService } = providerContext; @@ -20,7 +20,7 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider log.warning('These tests are being run with an external package registry'); } - const registryUrl = getRegistryUrl() ?? DEFAULT_REGISTRY_URL; + const registryUrl = getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); log.info(`Package registry URL for tests: ${registryUrl}`); before(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/registry.ts b/x-pack/test/security_solution_endpoint_api_int/registry.ts index cc474cbf29aaf..9a9d184b9c297 100644 --- a/x-pack/test/security_solution_endpoint_api_int/registry.ts +++ b/x-pack/test/security_solution_endpoint_api_int/registry.ts @@ -57,7 +57,7 @@ export function createEndpointDockerConfig( }); } -export function getRegistryUrl(): string | undefined { +export function getRegistryUrlFromTestEnv(): string | undefined { let registryUrl: string | undefined; if (dockerRegistryPort !== undefined) { registryUrl = `--xpack.ingestManager.registryUrl=http://localhost:${dockerRegistryPort}`; @@ -68,10 +68,10 @@ export function getRegistryUrl(): string | undefined { } export function getRegistryUrlAsArray(): string[] { - const registryUrl: string | undefined = getRegistryUrl(); + const registryUrl: string | undefined = getRegistryUrlFromTestEnv(); return registryUrl !== undefined ? [registryUrl] : []; } export function isRegistryEnabled() { - return getRegistryUrl() !== undefined; + return getRegistryUrlFromTestEnv() !== undefined; } From 84884a93981f683a583cc96e52f63c50dacb13b2 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 30 Jul 2020 19:16:51 -0400 Subject: [PATCH 17/55] [Security Solution][Lists] - Tests cleanup and remove unnecessary import (#73865) ## Summary Addresses feedback from https://github.com/elastic/kibana/pull/72748 - Updates `plugins/lists` tests text from `should not validate` to `should FAIL validation` after feedback that previous text is a bit confusing and can be interpreted to mean that validation is not conducted - Remove unnecessary spreads from one of my late night PRs - Removes `siem_common_deps` in favor of `shared_imports` in `plugins/lists` - Updates `build_exceptions_query.test.ts` to use existing mocks --- .../common/schemas/common/schemas.test.ts | 2 +- .../lists/common/schemas/common/schemas.ts | 2 +- .../search_es_list_item_schema.test.ts | 4 +- .../search_es_list_schema.test.ts | 4 +- .../create_endpoint_list_item_schema.test.ts | 2 +- .../create_endpoint_list_item_schema.ts | 2 +- .../create_exception_list_item_schema.test.ts | 2 +- .../create_exception_list_item_schema.ts | 2 +- .../create_exception_list_schema.test.ts | 2 +- .../request/create_exception_list_schema.ts | 2 +- .../request/create_list_item_schema.test.ts | 2 +- .../request/create_list_schema.test.ts | 2 +- .../schemas/request/create_list_schema.ts | 2 +- .../delete_endpoint_list_item_schema.test.ts | 2 +- .../delete_exception_list_item_schema.test.ts | 2 +- .../delete_exception_list_schema.test.ts | 2 +- .../request/delete_list_item_schema.test.ts | 2 +- .../request/delete_list_schema.test.ts | 2 +- .../export_list_item_query_schema.test.ts | 2 +- .../find_endpoint_list_item_schema.test.ts | 2 +- .../find_exception_list_item_schema.test.ts | 2 +- .../find_exception_list_schema.test.ts | 2 +- .../request/find_list_item_schema.test.ts | 2 +- .../schemas/request/find_list_schema.test.ts | 2 +- .../import_list_item_query_schema.test.ts | 2 +- .../request/import_list_item_schema.test.ts | 2 +- .../request/patch_list_item_schema.test.ts | 2 +- .../schemas/request/patch_list_schema.test.ts | 2 +- .../read_endpoint_list_item_schema.test.ts | 2 +- .../read_exception_list_item_schema.test.ts | 2 +- .../read_exception_list_schema.test.ts | 2 +- .../request/read_list_item_schema.test.ts | 2 +- .../schemas/request/read_list_schema.test.ts | 2 +- .../update_endpoint_list_item_schema.test.ts | 2 +- .../update_exception_list_item_schema.test.ts | 2 +- .../update_exception_list_schema.test.ts | 2 +- .../request/update_list_item_schema.test.ts | 2 +- .../response/acknowledge_schema.test.ts | 2 +- .../create_endpoint_list_schema.test.ts | 2 +- .../exception_list_item_schema.test.ts | 2 +- .../response/exception_list_schema.test.ts | 2 +- .../found_exception_list_item_schema.test.ts | 2 +- .../found_exception_list_schema.test.ts | 2 +- .../list_item_index_exist_schema.test.ts | 2 +- .../schemas/response/list_item_schema.test.ts | 2 +- .../schemas/response/list_schema.test.ts | 2 +- .../common/schemas/types/comment.test.ts | 2 +- .../lists/common/schemas/types/comment.ts | 2 +- .../schemas/types/create_comment.test.ts | 2 +- .../common/schemas/types/create_comment.ts | 2 +- .../types/default_comments_array.test.ts | 2 +- .../default_create_comments_array.test.ts | 2 +- .../schemas/types/default_namespace.test.ts | 4 +- .../types/default_namespace_array.test.ts | 6 +- .../default_update_comments_array.test.ts | 2 +- .../schemas/types/empty_string_array.test.ts | 4 +- .../common/schemas/types/entries.mock.ts | 23 +- .../common/schemas/types/entries.test.ts | 26 +- .../common/schemas/types/entry_exists.test.ts | 16 +- .../common/schemas/types/entry_exists.ts | 2 +- .../common/schemas/types/entry_list.test.ts | 18 +- .../lists/common/schemas/types/entry_list.ts | 2 +- .../common/schemas/types/entry_match.test.ts | 20 +- .../lists/common/schemas/types/entry_match.ts | 2 +- .../schemas/types/entry_match_any.test.ts | 20 +- .../common/schemas/types/entry_match_any.ts | 2 +- .../common/schemas/types/entry_nested.mock.ts | 2 +- .../common/schemas/types/entry_nested.test.ts | 20 +- .../common/schemas/types/entry_nested.ts | 2 +- .../types/non_empty_entries_array.test.ts | 20 +- .../non_empty_nested_entries_array.test.ts | 26 +- ...non_empty_or_nullable_string_array.test.ts | 12 +- .../types/non_empty_string_array.test.ts | 10 +- .../schemas/types/update_comment.test.ts | 2 +- .../common/schemas/types/update_comment.ts | 2 +- .../plugins/lists/common/siem_common_deps.ts | 9 - x-pack/plugins/lists/public/exceptions/api.ts | 2 +- x-pack/plugins/lists/public/lists/api.ts | 2 +- .../routes/create_endpoint_list_item_route.ts | 2 +- .../routes/create_endpoint_list_route.ts | 2 +- .../create_exception_list_item_route.ts | 2 +- .../routes/create_exception_list_route.ts | 2 +- .../server/routes/create_list_index_route.ts | 2 +- .../server/routes/create_list_item_route.ts | 2 +- .../lists/server/routes/create_list_route.ts | 2 +- .../routes/delete_endpoint_list_item_route.ts | 2 +- .../delete_exception_list_item_route.ts | 2 +- .../routes/delete_exception_list_route.ts | 2 +- .../server/routes/delete_list_index_route.ts | 2 +- .../server/routes/delete_list_item_route.ts | 2 +- .../lists/server/routes/delete_list_route.ts | 2 +- .../routes/find_endpoint_list_item_route.ts | 2 +- .../routes/find_exception_list_item_route.ts | 2 +- .../routes/find_exception_list_route.ts | 2 +- .../server/routes/find_list_item_route.ts | 2 +- .../lists/server/routes/find_list_route.ts | 2 +- .../server/routes/import_list_item_route.ts | 2 +- .../server/routes/patch_list_item_route.ts | 2 +- .../lists/server/routes/patch_list_route.ts | 2 +- .../routes/read_endpoint_list_item_route.ts | 2 +- .../routes/read_exception_list_item_route.ts | 2 +- .../routes/read_exception_list_route.ts | 2 +- .../server/routes/read_list_index_route.ts | 2 +- .../server/routes/read_list_item_route.ts | 2 +- .../lists/server/routes/read_list_route.ts | 2 +- .../routes/update_endpoint_list_item_route.ts | 2 +- .../update_exception_list_item_route.ts | 2 +- .../routes/update_exception_list_route.ts | 2 +- .../server/routes/update_list_item_route.ts | 2 +- .../lists/server/routes/update_list_route.ts | 2 +- .../plugins/lists/server/routes/validate.ts | 2 +- .../services/utils/encode_decode_cursor.ts | 2 +- .../build_exceptions_query.test.ts | 294 ++++++++---------- .../exceptions/builder/helpers.test.tsx | 4 +- .../components/exceptions/helpers.test.tsx | 2 +- 115 files changed, 340 insertions(+), 392 deletions(-) delete mode 100644 x-pack/plugins/lists/common/siem_common_deps.ts diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts index d450debd56293..fad8ecc86277b 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { EsDataTypeGeoPoint, diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 26511f89c32b8..76aa896a741f6 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import { DefaultNamespace } from '../types/default_namespace'; -import { DefaultStringArray, NonEmptyString } from '../../siem_common_deps'; +import { DefaultStringArray, NonEmptyString } from '../../shared_imports'; export const name = t.string; export type Name = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts index 7ac75b077acb5..d8e3793ac9bd6 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { SearchEsListItemSchema, searchEsListItemSchema } from './search_es_list_item_schema'; import { getSearchEsListItemMock } from './search_es_list_item_schema.mock'; @@ -22,7 +22,7 @@ describe('search_es_list_item_schema', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate with a madeup value', () => { + test('it should FAIL validation when a madeup value', () => { const payload: SearchEsListItemSchema & { madeupValue: string } = { ...getSearchEsListItemMock(), madeupValue: 'madeupvalue', diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts index 739f102e6a872..27a6c5ef52460 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { SearchEsListSchema, searchEsListSchema } from './search_es_list_schema'; import { getSearchEsListMock } from './search_es_list_schema.mock'; @@ -22,7 +22,7 @@ describe('search_es_list_schema', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate with a madeup value', () => { + test('it should FAIL validation when a madeup value', () => { const payload: SearchEsListSchema & { madeupValue: string } = { ...getSearchEsListMock(), madeupValue: 'madeupvalue', diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts index 75e0410be610a..e40a80a0d589d 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; import { CommentsArray } from '../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts index ab30e8e35548d..626b9e3e624ef 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts @@ -22,7 +22,7 @@ import { import { RequiredKeepUndefined } from '../../types'; import { CreateCommentsArray, DefaultCreateCommentsArray, nonEmptyEntriesArray } from '../types'; import { EntriesArray } from '../types/entries'; -import { DefaultUuid } from '../../siem_common_deps'; +import { DefaultUuid } from '../../shared_imports'; export const createEndpointListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts index cf4c1fea0306f..d2ad69d1ee7b6 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; import { CommentsArray } from '../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index c3f41cac90c64..039a38594a367 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -29,7 +29,7 @@ import { nonEmptyEntriesArray, } from '../types'; import { EntriesArray } from '../types/entries'; -import { DefaultUuid } from '../../siem_common_deps'; +import { DefaultUuid } from '../../shared_imports'; export const createExceptionListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts index 21270f526900b..c9e2aa37a132b 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { CreateExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 94a4e1588f5ab..7009fbd709e54 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -25,7 +25,7 @@ import { DefaultUuid, DefaultVersionNumber, DefaultVersionNumberDecoded, -} from '../../siem_common_deps'; +} from '../../shared_imports'; import { NamespaceType } from '../types'; export const createExceptionListSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts index 8178d49690e39..813d5e349e7e6 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateListItemSchemaMock } from './create_list_item_schema.mock'; import { CreateListItemSchema, createListItemSchema } from './create_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts index 9b496a01045de..82340453a98f1 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { CreateListSchema, createListSchema } from './create_list_schema'; import { getCreateListSchemaMock } from './create_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts index 18ed0f42ccd6f..bfe3ecdcb623b 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { description, deserializer, id, meta, name, serializer, type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { DefaultVersionNumber, DefaultVersionNumberDecoded } from '../../siem_common_deps'; +import { DefaultVersionNumber, DefaultVersionNumberDecoded } from '../../shared_imports'; export const createListSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts index fa75be8bc541e..fa3c1ef3b02f5 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteEndpointListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts index 042f62a8d129b..d249cd779e862 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteExceptionListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts index 2bb0a23173bd6..ec781d59af120 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts index 9bc2825d774ed..7b2263863e1f6 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteListItemSchema, deleteListItemSchema } from './delete_list_item_schema'; import { getDeleteListItemSchemaMock } from './delete_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts index 278508305c6f0..65ca2f3f457e9 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteListSchema, deleteListSchema } from './delete_list_schema'; import { getDeleteListSchemaMock } from './delete_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts index 1ffe2e2fc4ecc..cd6f4c1b147db 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ExportListItemQuerySchema, diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts index 8249b1e2d49c2..79449b136d066 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindEndpointListItemSchemaDecodedMock, diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts index f402f22b093ad..1e971a4eebc33 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { LIST_ID } from '../../constants.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts index ef96346c732b8..6f5d34d6be73e 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindExceptionListSchemaDecodedMock, diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts index 59d4b4485b578..8c119aeb14e24 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { LIST_ID } from '../../constants.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts index 63f29a64b4bf9..086e457e8f6b8 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindListSchemaDecodedMock, getFindListSchemaMock } from './find_list_schema.mock'; import { FindListSchemaEncoded, findListSchema } from './find_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts index 9d03229b4d1d9..9945dc03c2e14 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ImportListItemQuerySchema, diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts index 7f7c6368a1c5e..4de77b66610d3 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ImportListItemSchema, importListItemSchema } from './import_list_item_schema'; import { getImportListItemSchemaMock } from './import_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts index 58c19e8f9cb4f..b148f19da8a86 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getPathListItemSchemaMock } from './patch_list_item_schema.mock'; import { PatchListItemSchema, patchListItemSchema } from './patch_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts index 3ab658014bbfa..dea48df3f1702 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getPathListSchemaMock } from './patch_list_schema.mock'; import { PatchListSchema, patchListSchema } from './patch_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts index 70a1d783c87d6..adec476ea5ad7 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadEndpointListItemSchemaMock } from './read_endpoint_list_item_schema.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts index 86c80a527be0d..b7c2715f14e1c 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadExceptionListItemSchemaMock } from './read_exception_list_item_schema.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts index 86cebc3cd3f8e..3bc61e3a5e90a 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadExceptionListSchemaMock } from './read_exception_list_schema.mock'; import { ReadExceptionListSchema, readExceptionListSchema } from './read_exception_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts index 5c71c9820cc1e..1d140719ad939 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadListItemSchemaMock } from './read_list_item_schema.mock'; import { ReadListItemSchema, readListItemSchema } from './read_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts index a1ba2655dd723..0b7e92c23f77a 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadListSchemaMock } from './read_list_schema.mock'; import { ReadListSchema, readListSchema } from './read_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts index db5bc45ad028b..ecbbb250a88f6 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateEndpointListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts index ce589fb097a60..a49a5552603fd 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateExceptionListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts index 892f277045a69..650cbd439ad2b 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts index 6127e20343834..cb6cd76dd3f03 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateListItemSchema, updateListItemSchema } from './update_list_item_schema'; import { getUpdateListItemSchemaMock } from './update_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts index 6e7fb158767b5..a59a93b06e34d 100644 --- a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getAcknowledgeSchemaResponseMock } from './acknowledge_schema.mock'; import { AcknowledgeSchema, acknowledgeSchema } from './acknowledge_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts index 5fccaaac22e3a..8c1392109979e 100644 --- a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { CreateEndpointListSchema, createEndpointListSchema } from './create_endpoint_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts index c8bf73cf842e1..32b55104e4fdf 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; import { ExceptionListItemSchema, exceptionListItemSchema } from './exception_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts index b773dd498ed01..1b5ef08b02d5f 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { ExceptionListSchema, exceptionListSchema } from './exception_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts index 70fcf9a86122c..5da3accccd9c2 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; import { getFoundExceptionListItemSchemaMock } from './found_exception_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts index a96ee07c4613b..d4fa8ee0e3481 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { getFoundExceptionListSchemaMock } from './found_exception_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts index 9cb130ec0e8ad..2b072d8f95cd8 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListItemIndexExistSchemaResponseMock } from './list_item_index_exist_schema.mock'; import { ListItemIndexExistSchema, listItemIndexExistSchema } from './list_item_index_exist_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts index 8b73506d13750..ec4c8d2c2d1ea 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListItemResponseMock } from './list_item_schema.mock'; import { ListItemSchema, listItemSchema } from './list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts index e7ae9b45a5e15..87e56e5dd95ac 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListResponseMock } from './list_schema.mock'; import { ListSchema, listSchema } from './list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/types/comment.test.ts b/x-pack/plugins/lists/common/schemas/types/comment.test.ts index c7c945277f756..081bb9b4bae54 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DATE_NOW } from '../../constants.mock'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getCommentsArrayMock, getCommentsMock } from './comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/comment.ts b/x-pack/plugins/lists/common/schemas/types/comment.ts index 6b0b0166b9ee1..4d7aba3b3ad98 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { created_at, created_by, id, updated_at, updated_by } from '../common/schemas'; export const comment = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts index 366bf84d48bbf..8bca8df437871 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock, getCreateCommentsMock } from './create_comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.ts index fd33313430ce6..4ccc28b2c4a6d 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; export const createComment = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts index 541b8ab1c799c..ee2dc0cf2a478 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultCommentsArray } from './default_comments_array'; import { CommentsArray } from './comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts index eb960b5411904..4aac3cc84a3a2 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultCreateCommentsArray } from './default_create_comments_array'; import { CreateCommentsArray } from './create_comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts index 152f85233aa1a..8e7ffdbdaea7b 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultNamespace } from './default_namespace'; @@ -48,7 +48,7 @@ describe('default_namespace', () => { expect(message.schema).toEqual('single'); }); - test('it should NOT validate if not "single" or "agnostic"', () => { + test('it should FAIL validation if not "single" or "agnostic"', () => { const payload = 'something else'; const decoded = DefaultNamespace.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts index 255c89959b610..e377faae87947 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultNamespaceArray, DefaultNamespaceArrayType } from './default_namespace_array'; @@ -21,7 +21,7 @@ describe('default_namespace_array', () => { expect(message.schema).toEqual(['single']); }); - test('it should NOT validate a numeric value', () => { + test('it should FAIL validation of numeric value', () => { const payload = 5; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -86,7 +86,7 @@ describe('default_namespace_array', () => { expect(message.schema).toEqual(['single', 'agnostic', 'single']); }); - test('it should not validate 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { + test('it should FAIL validation when given 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { const payload: DefaultNamespaceArrayType = 'single,agnostic,junk'; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts index 612148dc4ccab..25c84af8c9ee3 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultUpdateCommentsArray } from './default_update_comments_array'; import { UpdateCommentsArray } from './update_comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts index b14afab327fb0..3ddeeebfceda7 100644 --- a/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { EmptyStringArray, EmptyStringArrayEncoded } from './empty_string_array'; @@ -57,7 +57,7 @@ describe('empty_string_array', () => { expect(message.schema).toEqual(['a', 'b', 'c']); }); - test('it should NOT validate a number', () => { + test('it should FAIL validation of number', () => { const payload: number = 5; const decoded = EmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts index 16794415138b2..c0093ed750b62 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts @@ -12,21 +12,18 @@ import { getEntryExistsMock } from './entry_exists.mock'; import { getEntryNestedMock } from './entry_nested.mock'; export const getListAndNonListEntriesArrayMock = (): EntriesArray => [ - { ...getEntryMatchMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryListMock() }, - { ...getEntryExistsMock() }, - { ...getEntryNestedMock() }, + getEntryMatchMock(), + getEntryMatchAnyMock(), + getEntryListMock(), + getEntryExistsMock(), + getEntryNestedMock(), ]; -export const getListEntriesArrayMock = (): EntriesArray => [ - { ...getEntryListMock() }, - { ...getEntryListMock() }, -]; +export const getListEntriesArrayMock = (): EntriesArray => [getEntryListMock(), getEntryListMock()]; export const getEntriesArrayMock = (): EntriesArray => [ - { ...getEntryMatchMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryExistsMock() }, - { ...getEntryNestedMock() }, + getEntryMatchMock(), + getEntryMatchAnyMock(), + getEntryExistsMock(), + getEntryNestedMock(), ]; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.test.ts b/x-pack/plugins/lists/common/schemas/types/entries.test.ts index cad94220a232c..f5c022c7a394f 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -20,7 +20,7 @@ import { entriesArray, entriesArrayOrUndefined, entry } from './entries'; describe('Entries', () => { describe('entry', () => { test('it should validate a match entry', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -29,7 +29,7 @@ describe('Entries', () => { }); test('it should validate a match_any entry', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -38,7 +38,7 @@ describe('Entries', () => { }); test('it should validate a exists entry', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -47,7 +47,7 @@ describe('Entries', () => { }); test('it should validate a list entry', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -55,8 +55,8 @@ describe('Entries', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate a nested entry', () => { - const payload = { ...getEntryNestedMock() }; + test('it should FAIL validation of nested entry', () => { + const payload = getEntryNestedMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -79,7 +79,7 @@ describe('Entries', () => { describe('entriesArray', () => { test('it should validate an array with match entry', () => { - const payload = [{ ...getEntryMatchMock() }]; + const payload = [getEntryMatchMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -88,7 +88,7 @@ describe('Entries', () => { }); test('it should validate an array with match_any entry', () => { - const payload = [{ ...getEntryMatchAnyMock() }]; + const payload = [getEntryMatchAnyMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -97,7 +97,7 @@ describe('Entries', () => { }); test('it should validate an array with exists entry', () => { - const payload = [{ ...getEntryExistsMock() }]; + const payload = [getEntryExistsMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -106,7 +106,7 @@ describe('Entries', () => { }); test('it should validate an array with list entry', () => { - const payload = [{ ...getEntryListMock() }]; + const payload = [getEntryListMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -115,7 +115,7 @@ describe('Entries', () => { }); test('it should validate an array with nested entry', () => { - const payload = [{ ...getEntryNestedMock() }]; + const payload = [getEntryNestedMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -144,7 +144,7 @@ describe('Entries', () => { }); test('it should validate an array with nested entry', () => { - const payload = [{ ...getEntryNestedMock() }]; + const payload = [getEntryNestedMock()]; const decoded = entriesArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts index 9d5b669333db8..0eb35b0768cf4 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryExistsMock } from './entry_exists.mock'; import { EntryExists, entriesExists } from './entry_exists'; describe('entriesExists', () => { test('it should validate an entry', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesExists', () => { }); test('it should validate when "operator" is "included"', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesExists', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); payload.operator = 'excluded'; const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesExists', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string } = { ...getEntryExistsMock(), field: '', @@ -56,16 +56,16 @@ describe('entriesExists', () => { test('it should strip out extra keys', () => { const payload: EntryExists & { extraKey?: string; - } = { ...getEntryExistsMock() }; + } = getEntryExistsMock(); payload.extraKey = 'some extra key'; const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryExistsMock() }); + expect(message.schema).toEqual(getEntryExistsMock()); }); - test('it should not validate when "type" is not "exists"', () => { + test('it should FAIL validation when "type" is not "exists"', () => { const payload: Omit & { type: string } = { ...getEntryExistsMock(), type: 'match', diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.ts index 05c82d2532218..4d9c09cc93574 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; export const entriesExists = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts index 14857edad5e3b..834fed3550e3f 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryListMock } from './entry_list.mock'; import { EntryList, entriesList } from './entry_list'; describe('entriesList', () => { test('it should validate an entry', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesList', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesList', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); payload.operator = 'excluded'; const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesList', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "list" is not expected value', () => { + test('it should FAIL validation when "list" is not expected value', () => { const payload: Omit & { list: string } = { ...getEntryListMock(), list: 'someListId', @@ -55,7 +55,7 @@ describe('entriesList', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "list.id" is empty string', () => { + test('it should FAIL validation when "list.id" is empty string', () => { const payload: Omit & { list: { id: string; type: 'ip' } } = { ...getEntryListMock(), list: { id: '', type: 'ip' }, @@ -67,7 +67,7 @@ describe('entriesList', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "lists"', () => { + test('it should FAIL validation when "type" is not "lists"', () => { const payload: Omit & { type: 'match_any' } = { ...getEntryListMock(), type: 'match_any', @@ -84,12 +84,12 @@ describe('entriesList', () => { test('it should strip out extra keys', () => { const payload: EntryList & { extraKey?: string; - } = { ...getEntryListMock() }; + } = getEntryListMock(); payload.extraKey = 'some extra key'; const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryListMock() }); + expect(message.schema).toEqual(getEntryListMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.ts index ae9de967db027..fcfec5e0cccdf 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator, type } from '../common/schemas'; export const entriesList = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts index 2c64592518eb7..7b49c418b547f 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { EntryMatch, entriesMatch } from './entry_match'; describe('entriesMatch', () => { test('it should validate an entry', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesMatch', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesMatch', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); payload.operator = 'excluded'; const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string } = { ...getEntryMatchMock(), field: '', @@ -53,7 +53,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "value" is not string', () => { + test('it should FAIL validation when "value" is not string', () => { const payload: Omit & { value: string[] } = { ...getEntryMatchMock(), value: ['some value'], @@ -67,7 +67,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "value" is empty string', () => { + test('it should FAIL validation when "value" is empty string', () => { const payload: Omit & { value: string } = { ...getEntryMatchMock(), value: '', @@ -79,7 +79,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "match"', () => { + test('it should FAIL validation when "type" is not "match"', () => { const payload: Omit & { type: string } = { ...getEntryMatchMock(), type: 'match_any', @@ -96,12 +96,12 @@ describe('entriesMatch', () => { test('it should strip out extra keys', () => { const payload: EntryMatch & { extraKey?: string; - } = { ...getEntryMatchMock() }; + } = getEntryMatchMock(); payload.extraKey = 'some value'; const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryMatchMock() }); + expect(message.schema).toEqual(getEntryMatchMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.ts index a21f83f317e35..247d64674e27d 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; export const entriesMatch = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts index 4dab2f45711f0..628ccfd74b606 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; import { EntryMatchAny, entriesMatchAny } from './entry_match_any'; describe('entriesMatchAny', () => { test('it should validate an entry', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesMatchAny', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesMatchAny', () => { }); test('it should validate when operator is "excluded"', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); payload.operator = 'excluded'; const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when field is empty string', () => { + test('it should FAIL validation when field is empty string', () => { const payload: Omit & { field: string } = { ...getEntryMatchAnyMock(), field: '', @@ -53,7 +53,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when value is empty array', () => { + test('it should FAIL validation when value is empty array', () => { const payload: Omit & { value: string[] } = { ...getEntryMatchAnyMock(), value: [], @@ -65,7 +65,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when value is not string array', () => { + test('it should FAIL validation when value is not string array', () => { const payload: Omit & { value: string } = { ...getEntryMatchAnyMock(), value: 'some string', @@ -79,7 +79,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "match_any"', () => { + test('it should FAIL validation when "type" is not "match_any"', () => { const payload: Omit & { type: string } = { ...getEntryMatchAnyMock(), type: 'match', @@ -94,12 +94,12 @@ describe('entriesMatchAny', () => { test('it should strip out extra keys', () => { const payload: EntryMatchAny & { extraKey?: string; - } = { ...getEntryMatchAnyMock() }; + } = getEntryMatchAnyMock(); payload.extraKey = 'some extra key'; const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryMatchAnyMock() }); + expect(message.schema).toEqual(getEntryMatchAnyMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts index e93ad4aa131d1..b6c4ef509c477 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts index f645bc9e40d78..d0e7712301ee1 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts @@ -11,7 +11,7 @@ import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; export const getEntryNestedMock = (): EntryNested => ({ - entries: [{ ...getEntryMatchMock() }, { ...getEntryMatchAnyMock() }], + entries: [getEntryMatchMock(), getEntryMatchAnyMock()], field: FIELD, type: NESTED, }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts index d9b58855413b1..d77440b207d03 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryNestedMock } from './entry_nested.mock'; import { EntryNested, entriesNested } from './entry_nested'; @@ -16,7 +16,7 @@ import { getEntryExistsMock } from './entry_exists.mock'; describe('entriesNested', () => { test('it should validate a nested entry', () => { - const payload = { ...getEntryNestedMock() }; + const payload = getEntryNestedMock(); const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -24,7 +24,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate when "type" is not "nested"', () => { + test('it should FAIL validation when "type" is not "nested"', () => { const payload: Omit & { type: 'match' } = { ...getEntryNestedMock(), type: 'match', @@ -36,7 +36,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string; } = { ...getEntryNestedMock(), field: '' }; @@ -47,7 +47,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "field" is not a string', () => { + test('it should FAIL validation when "field" is not a string', () => { const payload: Omit & { field: number; } = { ...getEntryNestedMock(), field: 1 }; @@ -58,7 +58,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "entries" is not a an array', () => { + test('it should FAIL validation when "entries" is not a an array', () => { const payload: Omit & { entries: string; } = { ...getEntryNestedMock(), entries: 'im a string' }; @@ -72,7 +72,7 @@ describe('entriesNested', () => { }); test('it should validate when "entries" contains an entry item that is type "match"', () => { - const payload = { ...getEntryNestedMock(), entries: [{ ...getEntryMatchAnyMock() }] }; + const payload = { ...getEntryNestedMock(), entries: [getEntryMatchAnyMock()] }; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -92,7 +92,7 @@ describe('entriesNested', () => { }); test('it should validate when "entries" contains an entry item that is type "exists"', () => { - const payload = { ...getEntryNestedMock(), entries: [{ ...getEntryExistsMock() }] }; + const payload = { ...getEntryNestedMock(), entries: [getEntryExistsMock()] }; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -113,12 +113,12 @@ describe('entriesNested', () => { test('it should strip out extra keys', () => { const payload: EntryNested & { extraKey?: string; - } = { ...getEntryNestedMock() }; + } = getEntryNestedMock(); payload.extraKey = 'some extra key'; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryNestedMock() }); + expect(message.schema).toEqual(getEntryNestedMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.ts index 9989f501d4338..f9e8e4356b811 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array'; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts index a2697286aa038..42d476a9fefb2 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -22,7 +22,7 @@ import { nonEmptyEntriesArray } from './non_empty_entries_array'; import { EntriesArray } from './entries'; describe('non_empty_entries_array', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: EntriesArray = []; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -33,7 +33,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -44,7 +44,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -56,7 +56,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchMock() }, { ...getEntryMatchMock() }]; + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -65,7 +65,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchAnyMock() }, { ...getEntryMatchAnyMock() }]; + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -74,7 +74,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [{ ...getEntryExistsMock() }, { ...getEntryExistsMock() }]; + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -92,7 +92,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "nested" entries', () => { - const payload: EntriesArray = [{ ...getEntryNestedMock() }, { ...getEntryNestedMock() }]; + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -109,7 +109,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of entries of value list and non-value list entries', () => { + test('it should FAIL validation when given an array of entries of value list and non-value list entries', () => { const payload: EntriesArray = [...getListAndNonListEntriesArrayMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -118,7 +118,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of non entries', () => { + test('it should FAIL validation when given an array of non entries', () => { const payload = [1]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts index 1154f2b6098da..7dbc3465610c0 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -17,7 +17,7 @@ import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array'; import { EntriesArray } from './entries'; describe('non_empty_nested_entries_array', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: EntriesArray = []; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -28,7 +28,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -39,7 +39,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -51,7 +51,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchMock() }, { ...getEntryMatchMock() }]; + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -60,7 +60,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchAnyMock() }, { ...getEntryMatchAnyMock() }]; + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -69,7 +69,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [{ ...getEntryExistsMock() }, { ...getEntryExistsMock() }]; + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -77,8 +77,8 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of "nested" entries', () => { - const payload: EntriesArray = [{ ...getEntryNestedMock() }, { ...getEntryNestedMock() }]; + test('it should FAIL validation when given an array of "nested" entries', () => { + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -105,9 +105,9 @@ describe('non_empty_nested_entries_array', () => { test('it should validate an array of entries', () => { const payload: EntriesArray = [ - { ...getEntryExistsMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryMatchMock() }, + getEntryExistsMock(), + getEntryMatchAnyMock(), + getEntryMatchMock(), ]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -116,7 +116,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of non entries', () => { + test('it should FAIL validation when given an array of non entries', () => { const payload = [1]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts index e3cc9104853e5..4b31b649556b2 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts @@ -7,12 +7,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array'; describe('nonEmptyOrNullableStringArray', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: string[] = []; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -34,7 +34,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -45,7 +45,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of with an empty string', () => { + test('it should FAIL validation when given an array of with an empty string', () => { const payload: string[] = ['im good', '']; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -56,7 +56,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of non strings', () => { + test('it should FAIL validation when given an array of non strings', () => { const payload = [1]; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts index fac088568f85e..db81b0d469859 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts @@ -7,12 +7,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { NonEmptyStringArray } from './non_empty_string_array'; describe('non_empty_string_array', () => { - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload: NonEmptyStringArray | null = null; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload: NonEmptyStringArray | undefined = undefined; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -34,7 +34,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate a single value of an empty string ""', () => { + test('it should FAIL validation of single value of an empty string ""', () => { const payload: NonEmptyStringArray = ''; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -72,7 +72,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual(['a', 'b', 'c']); }); - test('it should NOT validate a number', () => { + test('it should FAIL validation of number', () => { const payload: number = 5; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts index ac7716af40966..ac4d0304cbb8e 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getUpdateCommentMock, getUpdateCommentsArrayMock } from './update_comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.ts index b95812cb35bf9..dc14bf480857f 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { id } from '../common/schemas'; export const updateComment = t.intersection([ diff --git a/x-pack/plugins/lists/common/siem_common_deps.ts b/x-pack/plugins/lists/common/siem_common_deps.ts deleted file mode 100644 index 2b37e2b7bf106..0000000000000 --- a/x-pack/plugins/lists/common/siem_common_deps.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// DEPRECATED: Do not add exports to this file; please import from shared_imports instead - -export * from './shared_imports'; diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index d661cb103fad8..203c84b2943fd 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -29,7 +29,7 @@ import { updateExceptionListItemSchema, updateExceptionListSchema, } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { AddEndpointExceptionListProps, diff --git a/x-pack/plugins/lists/public/lists/api.ts b/x-pack/plugins/lists/public/lists/api.ts index 606109f1910c4..211b2445a0429 100644 --- a/x-pack/plugins/lists/public/lists/api.ts +++ b/x-pack/plugins/lists/public/lists/api.ts @@ -29,7 +29,7 @@ import { listSchema, } from '../../common/schemas'; import { LIST_INDEX, LIST_ITEM_URL, LIST_PRIVILEGES_URL, LIST_URL } from '../../common/constants'; -import { validateEither } from '../../common/siem_common_deps'; +import { validateEither } from '../../common/shared_imports'; import { toError, toPromise } from '../common/fp_utils'; import { diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts index 22aa1fb59858b..7fd07ed5fb8cd 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateEndpointListItemSchemaDecoded, createEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts index b1e589be67cd1..91b6a328c8649 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_URL } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { createEndpointListSchema } from '../../common/schemas'; import { getExceptionListClient } from './utils/get_exception_list_client'; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index ed58621dae973..fc0473b2b3704 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateExceptionListItemSchemaDecoded, createExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index fbe9c6ec9d83b..08db0825e07bd 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateExceptionListSchemaDecoded, createExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/create_list_index_route.ts index 1bffdd6bd5b5f..be08093dc7055 100644 --- a/x-pack/plugins/lists/server/routes/create_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -7,7 +7,7 @@ import { IRouter } from 'kibana/server'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { LIST_INDEX } from '../../common/constants'; import { acknowledgeSchema } from '../../common/schemas'; diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts index 656d6af2c6c9a..0a4a1c739ae7c 100644 --- a/x-pack/plugins/lists/server/routes/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -9,7 +9,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { createListItemSchema, listItemSchema } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts index 297dcfc49db34..90f5bf9b2c650 100644 --- a/x-pack/plugins/lists/server/routes/create_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateListSchemaDecoded, createListSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts index 2d5028bd9525a..380fdcf862060 100644 --- a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteEndpointListItemSchemaDecoded, deleteEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index 06ff051925407..07e0fad20c900 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteExceptionListItemSchemaDecoded, deleteExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index f2bf517f55ae3..769ce732240b7 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteExceptionListSchemaDecoded, deleteExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts index be58d8aeed17d..aa587273036ae 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { acknowledgeSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts index 50313cd1294ae..2284068552485 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { deleteListItemSchema, listItemArraySchema, listItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index 4eeb6d8f126ad..f87645b79fc75 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { deleteListSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts index 9f83761cc501a..d6a459b3ac961 100644 --- a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindEndpointListItemSchemaDecoded, findEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index 270aad85796b2..88643e53ff0a7 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindExceptionListItemSchemaDecoded, findExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index c5cae7a1e0bb8..41342261ef681 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindExceptionListSchemaDecoded, findExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts index 533dc74aa3694..454ea891857c3 100644 --- a/x-pack/plugins/lists/server/routes/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindListItemSchemaDecoded, findListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_list_route.ts b/x-pack/plugins/lists/server/routes/find_list_route.ts index 268eb36a5e26e..d751214006dcc 100644 --- a/x-pack/plugins/lists/server/routes/find_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { findListSchema, foundListSchema } from '../../common/schemas'; import { decodeCursor } from '../services/utils'; diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts index e162e7829e456..ce5fdaccae251 100644 --- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { importListItemQuerySchema, listSchema } from '../../common/schemas'; import { ConfigType } from '../config'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts index d975e80079ab7..58cca0313006d 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemSchema, patchListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts index 421f1279f2619..e33d8d7c9c598 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, patchListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts index fd932746ce990..e80347d97bb7a 100644 --- a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadEndpointListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index fe8256fbda5cd..0cfac6467f089 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index 0512876d298d4..d9359881616f4 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts index 87a4d85e0d254..5524c1beeaa52 100644 --- a/x-pack/plugins/lists/server/routes/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemIndexExistSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts index b7cf2b9f7123b..99d34d0fd84a6 100644 --- a/x-pack/plugins/lists/server/routes/read_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -9,7 +9,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listItemArraySchema, listItemSchema, readListItemSchema } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts index 4bce09ecd3bde..da3cf73b56819 100644 --- a/x-pack/plugins/lists/server/routes/read_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, readListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts index f717dc0fb3392..e0d6a0ffffa6b 100644 --- a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateEndpointListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index f5e0e7ae75700..7e15f694aee13 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index 6fcee81ed573f..bead10802df4f 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts index d479bc63b64bd..3490027b12747 100644 --- a/x-pack/plugins/lists/server/routes/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemSchema, updateListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts index 6206c0943a8f3..816ad13d3770e 100644 --- a/x-pack/plugins/lists/server/routes/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, updateListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/validate.ts b/x-pack/plugins/lists/server/routes/validate.ts index bbd4b0eaf0e33..a7f5c96e13d7b 100644 --- a/x-pack/plugins/lists/server/routes/validate.ts +++ b/x-pack/plugins/lists/server/routes/validate.ts @@ -8,7 +8,7 @@ import { ExceptionListClient } from '../services/exception_lists/exception_list_ import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants'; import { foundExceptionListItemSchema } from '../../common/schemas'; import { NamespaceType } from '../../common/schemas/types'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; export const validateExceptionListSize = async ( exceptionLists: ExceptionListClient, diff --git a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts index 205d61f204ba6..5c7243a1d15a3 100644 --- a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts +++ b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts @@ -9,7 +9,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { CursorOrUndefined, SortFieldOrUndefined } from '../../../common/schemas'; -import { exactCheck } from '../../../common/siem_common_deps'; +import { exactCheck } from '../../../common/shared_imports'; /** * Used only internally for this ad-hoc opaque cursor structure to keep track of the diff --git a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts index 2cebaacc67681..2d37d4a345fa1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts @@ -15,86 +15,13 @@ import { getLanguageBooleanOperator, buildNested, } from './build_exceptions_query'; -import { - EntryNested, - EntryExists, - EntryMatch, - EntryMatchAny, - EntriesArray, - Operator, -} from '../../../lists/common/schemas'; +import { EntryNested, EntryMatchAny, EntriesArray } from '../../../lists/common/schemas'; import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntryMatchMock } from '../../../lists/common/schemas/types/entry_match.mock'; import { getEntryMatchAnyMock } from '../../../lists/common/schemas/types/entry_match_any.mock'; import { getEntryExistsMock } from '../../../lists/common/schemas/types/entry_exists.mock'; describe('build_exceptions_query', () => { - const makeMatchEntry = ({ - field, - value = 'value-1', - operator = 'included', - }: { - field: string; - value?: string; - operator?: Operator; - }): EntryMatch => { - return { - field, - operator, - type: 'match', - value, - }; - }; - const makeMatchAnyEntry = ({ - field, - operator = 'included', - value = ['value-1', 'value-2'], - }: { - field: string; - operator?: Operator; - value?: string[]; - }): EntryMatchAny => { - return { - field, - operator, - value, - type: 'match_any', - }; - }; - const makeExistsEntry = ({ - field, - operator = 'included', - }: { - field: string; - operator?: Operator; - }): EntryExists => { - return { - field, - operator, - type: 'exists', - }; - }; - const matchEntryWithIncluded: EntryMatch = makeMatchEntry({ - field: 'host.name', - value: 'suricata', - }); - const matchEntryWithExcluded: EntryMatch = makeMatchEntry({ - field: 'host.name', - value: 'suricata', - operator: 'excluded', - }); - const matchAnyEntryWithIncludedAndTwoValues: EntryMatchAny = makeMatchAnyEntry({ - field: 'host.name', - value: ['suricata', 'auditd'], - }); - const existsEntryWithIncluded: EntryExists = makeExistsEntry({ - field: 'host.name', - }); - const existsEntryWithExcluded: EntryExists = makeExistsEntry({ - field: 'host.name', - operator: 'excluded', - }); - describe('getLanguageBooleanOperator', () => { test('it returns value as uppercase if language is "lucene"', () => { const result = getLanguageBooleanOperator({ language: 'lucene', value: 'not' }); @@ -137,14 +64,14 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { const query = buildExists({ - entry: existsEntryWithExcluded, + entry: { ...getEntryExistsMock(), operator: 'excluded' }, language: 'kuery', }); expect(query).toEqual('not host.name:*'); }); test('it returns formatted wildcard string when operator is "included"', () => { const query = buildExists({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'kuery', }); expect(query).toEqual('host.name:*'); @@ -154,14 +81,14 @@ describe('build_exceptions_query', () => { describe('lucene', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { const query = buildExists({ - entry: existsEntryWithExcluded, + entry: { ...getEntryExistsMock(), operator: 'excluded' }, language: 'lucene', }); expect(query).toEqual('NOT _exists_host.name'); }); test('it returns formatted wildcard string when operator is "included"', () => { const query = buildExists({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'lucene', }); expect(query).toEqual('_exists_host.name'); @@ -173,52 +100,55 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted string when operator is "included"', () => { const query = buildMatch({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'kuery', }); - expect(query).toEqual('host.name:"suricata"'); + expect(query).toEqual('host.name:"some host name"'); }); test('it returns formatted string when operator is "excluded"', () => { const query = buildMatch({ - entry: matchEntryWithExcluded, + entry: { ...getEntryMatchMock(), operator: 'excluded' }, language: 'kuery', }); - expect(query).toEqual('not host.name:"suricata"'); + expect(query).toEqual('not host.name:"some host name"'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { const query = buildMatch({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'lucene', }); - expect(query).toEqual('host.name:"suricata"'); + expect(query).toEqual('host.name:"some host name"'); }); test('it returns formatted string when operator is "excluded"', () => { const query = buildMatch({ - entry: matchEntryWithExcluded, + entry: { ...getEntryMatchMock(), operator: 'excluded' }, language: 'lucene', }); - expect(query).toEqual('NOT host.name:"suricata"'); + expect(query).toEqual('NOT host.name:"some host name"'); }); }); }); describe('buildMatchAny', () => { - const entryWithIncludedAndNoValues: EntryMatchAny = makeMatchAnyEntry({ + const entryWithIncludedAndNoValues: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', value: [], - }); - const entryWithIncludedAndOneValue: EntryMatchAny = makeMatchAnyEntry({ + }; + const entryWithIncludedAndOneValue: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', - value: ['suricata'], - }); - const entryWithExcludedAndTwoValues: EntryMatchAny = makeMatchAnyEntry({ + value: ['some host name'], + }; + const entryWithExcludedAndTwoValues: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', - value: ['suricata', 'auditd'], + value: ['some host name', 'auditd'], operator: 'excluded', - }); + }; describe('kuery', () => { test('it returns empty string if given an empty array for "values"', () => { @@ -235,16 +165,16 @@ describe('build_exceptions_query', () => { language: 'kuery', }); - expect(exceptionSegment).toEqual('host.name:("suricata")'); + expect(exceptionSegment).toEqual('host.name:("some host name")'); }); test('it returns formatted string when operator is "included"', () => { const exceptionSegment = buildMatchAny({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'kuery', }); - expect(exceptionSegment).toEqual('host.name:("suricata" or "auditd")'); + expect(exceptionSegment).toEqual('host.name:("some host name" or "auditd")'); }); test('it returns formatted string when operator is "excluded"', () => { @@ -253,18 +183,18 @@ describe('build_exceptions_query', () => { language: 'kuery', }); - expect(exceptionSegment).toEqual('not host.name:("suricata" or "auditd")'); + expect(exceptionSegment).toEqual('not host.name:("some host name" or "auditd")'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { const exceptionSegment = buildMatchAny({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'lucene', }); - expect(exceptionSegment).toEqual('host.name:("suricata" OR "auditd")'); + expect(exceptionSegment).toEqual('host.name:("some host name" OR "auditd")'); }); test('it returns formatted string when operator is "excluded"', () => { const exceptionSegment = buildMatchAny({ @@ -272,7 +202,7 @@ describe('build_exceptions_query', () => { language: 'lucene', }); - expect(exceptionSegment).toEqual('NOT host.name:("suricata" OR "auditd")'); + expect(exceptionSegment).toEqual('NOT host.name:("some host name" OR "auditd")'); }); test('it returns formatted string when "values" includes only one item', () => { const exceptionSegment = buildMatchAny({ @@ -280,7 +210,7 @@ describe('build_exceptions_query', () => { language: 'lucene', }); - expect(exceptionSegment).toEqual('host.name:("suricata")'); + expect(exceptionSegment).toEqual('host.name:("some host name")'); }); }); }); @@ -394,7 +324,7 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { const result = buildEntry({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'kuery', }); expect(result).toEqual('host.name:*'); @@ -402,25 +332,25 @@ describe('build_exceptions_query', () => { test('it returns formatted string when "type" is "match"', () => { const result = buildEntry({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'kuery', }); - expect(result).toEqual('host.name:"suricata"'); + expect(result).toEqual('host.name:"some host name"'); }); test('it returns formatted string when "type" is "match_any"', () => { const result = buildEntry({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'kuery', }); - expect(result).toEqual('host.name:("suricata" or "auditd")'); + expect(result).toEqual('host.name:("some host name" or "auditd")'); }); }); describe('lucene', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { const result = buildEntry({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'lucene', }); expect(result).toEqual('_exists_host.name'); @@ -428,18 +358,18 @@ describe('build_exceptions_query', () => { test('it returns formatted string when "type" is "match"', () => { const result = buildEntry({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'lucene', }); - expect(result).toEqual('host.name:"suricata"'); + expect(result).toEqual('host.name:"some host name"'); }); test('it returns formatted string when "type" is "match_any"', () => { const result = buildEntry({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'lucene', }); - expect(result).toEqual('host.name:("suricata" OR "auditd")'); + expect(result).toEqual('host.name:("some host name" OR "auditd")'); }); }); }); @@ -456,26 +386,31 @@ describe('build_exceptions_query', () => { test('it returns expected query when more than one item in exception item', () => { const payload: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-3' }), + { ...getEntryMatchAnyMock(), field: 'b' }, + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'value-3' }, ]; const query = buildExceptionItem({ language: 'kuery', entries: payload, }); - const expectedQuery = 'b:("value-1" or "value-2") and not c:"value-3"'; + const expectedQuery = 'b:("some host name") and not c:"value-3"'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when exception item includes nested value', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'included', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'included', + value: 'value-3', + }, ], }, ]; @@ -483,56 +418,65 @@ describe('build_exceptions_query', () => { language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2") and parent:{ nestedField:"value-3" }'; + const expectedQuery = 'b:("some host name") and parent:{ nestedField:"value-3" }'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when exception item includes multiple items and nested "and" values', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'included', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'included', + value: 'value-3', + }, ], }, - makeExistsEntry({ field: 'd' }), + { ...getEntryExistsMock(), field: 'd' }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = - 'b:("value-1" or "value-2") and parent:{ nestedField:"value-3" } and d:*'; + const expectedQuery = 'b:("some host name") and parent:{ nestedField:"value-3" } and d:*'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when language is "lucene"', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'excluded', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'excluded', + value: 'value-3', + }, ], }, - makeExistsEntry({ field: 'e', operator: 'excluded' }), + { ...getEntryExistsMock(), field: 'e', operator: 'excluded' }, ]; const query = buildExceptionItem({ language: 'lucene', entries, }); const expectedQuery = - 'b:("value-1" OR "value-2") AND parent:{ NOT nestedField:"value-3" } AND NOT _exists_e'; + 'b:("some host name") AND parent:{ NOT nestedField:"value-3" } AND NOT _exists_e'; expect(query).toEqual(expectedQuery); }); describe('exists', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeExistsEntry({ field: 'b' })]; + const entries: EntriesArray = [{ ...getEntryExistsMock(), field: 'b' }]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -543,7 +487,9 @@ describe('build_exceptions_query', () => { }); test('it returns expected query when list includes single list item with operator of "excluded"', () => { - const entries: EntriesArray = [makeExistsEntry({ field: 'b', operator: 'excluded' })]; + const entries: EntriesArray = [ + { ...getEntryExistsMock(), field: 'b', operator: 'excluded' }, + ]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -555,11 +501,13 @@ describe('build_exceptions_query', () => { test('it returns expected query when exception item includes entry item with "and" values', () => { const entries: EntriesArray = [ - makeExistsEntry({ field: 'b', operator: 'excluded' }), + { ...getEntryExistsMock(), field: 'b', operator: 'excluded' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'value-1' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'value-1' }, + ], }, ]; const query = buildExceptionItem({ @@ -573,16 +521,16 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeExistsEntry({ field: 'b' }), + { ...getEntryExistsMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-1' }), - makeMatchEntry({ field: 'd', value: 'value-2' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'value-1' }, + { ...getEntryMatchMock(), field: 'd', value: 'value-2' }, ], }, - makeExistsEntry({ field: 'e' }), + { ...getEntryExistsMock(), field: 'e' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -596,7 +544,7 @@ describe('build_exceptions_query', () => { describe('match', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeMatchEntry({ field: 'b', value: 'value' })]; + const entries: EntriesArray = [{ ...getEntryMatchMock(), field: 'b', value: 'value' }]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -608,7 +556,7 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes single list item with operator of "excluded"', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', operator: 'excluded', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', operator: 'excluded', value: 'value' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -621,11 +569,13 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes list item with "and" values', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', operator: 'excluded', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', operator: 'excluded', value: 'value' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'valueC' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'valueC' }, + ], }, ]; const query = buildExceptionItem({ @@ -639,16 +589,16 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', value: 'value' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'excluded', value: 'valueD' }, ], }, - makeMatchEntry({ field: 'e', value: 'valueE' }), + { ...getEntryMatchMock(), field: 'e', value: 'valueE' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -663,55 +613,59 @@ describe('build_exceptions_query', () => { describe('match_any', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b' })]; + const entries: EntriesArray = [{ ...getEntryMatchAnyMock(), field: 'b' }]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2")'; + const expectedQuery = 'b:("some host name")'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes single list item with operator of "excluded"', () => { - const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b', operator: 'excluded' })]; + const entries: EntriesArray = [ + { ...getEntryMatchAnyMock(), field: 'b', operator: 'excluded' }, + ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'not b:("value-1" or "value-2")'; + const expectedQuery = 'not b:("some host name")'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes list item with nested values', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b', operator: 'excluded' }), + { ...getEntryMatchAnyMock(), field: 'b', operator: 'excluded' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + ], }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'not b:("value-1" or "value-2") and parent:{ not c:"valueC" }'; + const expectedQuery = 'not b:("some host name") and parent:{ not c:"valueC" }'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - makeMatchAnyEntry({ field: 'c' }), + { ...getEntryMatchAnyMock(), field: 'b' }, + { ...getEntryMatchAnyMock(), field: 'c' }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2") and c:("value-1" or "value-2")'; + const expectedQuery = 'b:("some host name") and c:("some host name")'; expect(query).toEqual(expectedQuery); }); @@ -735,16 +689,16 @@ describe('build_exceptions_query', () => { const payload = getExceptionListItemSchemaMock(); const payload2 = getExceptionListItemSchemaMock(); payload2.entries = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'included', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'included', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'included', value: 'valueD' }, ], }, - makeMatchAnyEntry({ field: 'e', operator: 'excluded' }), + { ...getEntryMatchAnyMock(), field: 'e', operator: 'excluded' }, ]; const queries = buildExceptionListQueries({ language: 'kuery', @@ -758,7 +712,7 @@ describe('build_exceptions_query', () => { }, { query: - 'b:("value-1" or "value-2") and parent:{ c:"valueC" and d:"valueD" } and not e:("value-1" or "value-2")', + 'b:("some host name") and parent:{ c:"valueC" and d:"valueD" } and not e:("some host name")', language: 'kuery', }, ]; @@ -768,20 +722,26 @@ describe('build_exceptions_query', () => { test('it returns expected query when lists exist and language is "lucene"', () => { const payload = getExceptionListItemSchemaMock(); - payload.entries = [makeMatchAnyEntry({ field: 'a' }), makeMatchAnyEntry({ field: 'b' })]; + payload.entries = [ + { ...getEntryMatchAnyMock(), field: 'a' }, + { ...getEntryMatchAnyMock(), field: 'b' }, + ]; const payload2 = getExceptionListItemSchemaMock(); - payload2.entries = [makeMatchAnyEntry({ field: 'c' }), makeMatchAnyEntry({ field: 'd' })]; + payload2.entries = [ + { ...getEntryMatchAnyMock(), field: 'c' }, + { ...getEntryMatchAnyMock(), field: 'd' }, + ]; const queries = buildExceptionListQueries({ language: 'lucene', lists: [payload, payload2], }); const expectedQueries = [ { - query: 'a:("value-1" OR "value-2") AND b:("value-1" OR "value-2")', + query: 'a:("some host name") AND b:("some host name")', language: 'lucene', }, { - query: 'c:("value-1" OR "value-2") AND d:("value-1" OR "value-2")', + query: 'c:("some host name") AND d:("some host name")', language: 'lucene', }, ]; @@ -793,17 +753,17 @@ describe('build_exceptions_query', () => { const payload = getExceptionListItemSchemaMock(); const payload2 = getExceptionListItemSchemaMock(); payload2.entries = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ // TODO: these operators are not being respected. buildNested needs to be updated - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'excluded', value: 'valueD' }, ], }, - makeMatchAnyEntry({ field: 'e' }), + { ...getEntryMatchAnyMock(), field: 'e' }, ]; const queries = buildExceptionListQueries({ language: 'kuery', @@ -817,7 +777,7 @@ describe('build_exceptions_query', () => { }, { query: - 'b:("value-1" or "value-2") and parent:{ not c:"valueC" and not d:"valueD" } and e:("value-1" or "value-2")', + 'b:("some host name") and parent:{ not c:"valueC" and not d:"valueD" } and e:("some host name")', language: 'kuery', }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx index e8a5196a418d6..224c99756eb5c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx @@ -466,7 +466,7 @@ describe('Exception builder helpers', () => { describe('#isEntryNested', () => { test('it returns "false" if payload is not of type EntryNested', () => { - const payload: BuilderEntry = { ...getEntryMatchMock() }; + const payload: BuilderEntry = getEntryMatchMock(); const output = isEntryNested(payload); const expected = false; expect(output).toEqual(expected); @@ -483,7 +483,7 @@ describe('Exception builder helpers', () => { describe('#getFormattedBuilderEntries', () => { test('it returns formatted entry with field undefined if it unable to find a matching index pattern field', () => { const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItems: BuilderEntry[] = [{ ...getEntryMatchMock() }]; + const payloadItems: BuilderEntry[] = [getEntryMatchMock()]; const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems); const expected: FormattedBuilderEntry[] = [ { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 18b509d16b352..4236f347ac7ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -365,7 +365,7 @@ describe('Exception helpers', () => { const mockEmptyException: EntryNested = { field: '', type: OperatorTypeEnum.NESTED, - entries: [{ ...getEntryMatchMock() }], + entries: [getEntryMatchMock()], }; const output: Array< ExceptionListItemSchema | CreateExceptionListItemSchema From aa668db796ccc0fd63bb41b94548740305106cce Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 30 Jul 2020 19:01:46 -0700 Subject: [PATCH 18/55] [Metrics UI] Fix alert management to open without refresh (#73739) * [Metrics UI] Fix alert management to open without refresh * removing unecessary code * Deleting unused imports Co-authored-by: Elastic Machine --- .../inventory/components/alert_dropdown.tsx | 31 +++++-------------- .../manage_alerts_context_menu_item.tsx | 22 +++++++++++++ .../components/alert_dropdown.tsx | 31 +++++-------------- 3 files changed, 38 insertions(+), 46 deletions(-) create mode 100644 x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx index 04642a01c15b4..ce0911666f0db 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; import { AlertFlyout } from './alert_flyout'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ManageAlertsContextMenuItem } from './manage_alerts_context_menu_item'; export const InventoryAlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); - const kibana = useKibana(); const { inventoryPrefill } = useAlertPrefillContext(); const { nodeType, metric, filterQuery } = inventoryPrefill; @@ -27,26 +26,12 @@ export const InventoryAlertDropdown = () => { setPopoverOpen(true); }, [setPopoverOpen]); - const menuItems = useMemo(() => { - return [ - setFlyoutVisible(true)}> - - , - - - , - ]; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [kibana.services]); + const menuItems = [ + setFlyoutVisible(true)}> + + , + , + ]; return ( <> diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx new file mode 100644 index 0000000000000..fc565aee37ff4 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiContextMenuItem } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +export const ManageAlertsContextMenuItem = () => { + const manageAlertsLinkProps = useLinkProps({ + app: 'management', + pathname: '/insightsAndAlerting/triggersActions/alerts', + }); + return ( + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx index 384a93e796dbe..dd61be0eee362 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useAlertPrefillContext } from '../../use_alert_prefill'; import { AlertFlyout } from './alert_flyout'; +import { ManageAlertsContextMenuItem } from '../../inventory/components/manage_alerts_context_menu_item'; export const MetricsAlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); - const kibana = useKibana(); const { metricThresholdPrefill } = useAlertPrefillContext(); const { groupBy, filterQuery, metrics } = metricThresholdPrefill; @@ -27,26 +26,12 @@ export const MetricsAlertDropdown = () => { setPopoverOpen(true); }, [setPopoverOpen]); - const menuItems = useMemo(() => { - return [ - setFlyoutVisible(true)}> - - , - - - , - ]; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [kibana.services]); + const menuItems = [ + setFlyoutVisible(true)}> + + , + , + ]; return ( <> From 0a5427842b8f4810a9a2a44fbe625d1d50c3e070 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 30 Jul 2020 21:08:57 -0600 Subject: [PATCH 19/55] [SIEM] Fixes "include building block button" to operate (#73900) ## Summary Blocker fixes "include building block button" to operate when there is no data on the table. Before if you had nothing on the table then the button would not operate as it would not cause a re-render: ![button_not_working](https://user-images.githubusercontent.com/1151048/88980376-cde1de00-d280-11ea-98cf-b67ef9fe9f72.gif) After where the button now works: ![button_working](https://user-images.githubusercontent.com/1151048/88980385-d3d7bf00-d280-11ea-89e4-f806e62853ed.gif) This wasn't caught because most people have something already on the table which makes the rendering render and just work. Simple one line low level fix. ### Checklist Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios No tests for this file at the moment and we need this as a fast backport to make the release cut off. --- .../alerts_utility_bar/index.test.tsx | 188 +++++++++++++++++- .../alerts_table/alerts_utility_bar/index.tsx | 5 +- 2 files changed, 188 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx index cbbe43cc03568..0ba9764cf24af 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -5,14 +5,15 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; -import { AlertsUtilityBar } from './index'; +import { AlertsUtilityBar, AlertsUtilityBarProps } from './index'; +import { TestProviders } from '../../../../common/mock/test_providers'; jest.mock('../../../../common/lib/kibana'); describe('AlertsUtilityBar', () => { - it('renders correctly', () => { + test('renders correctly', () => { const wrapper = shallow( { expect(wrapper.find('[dataTestSubj="alertActionPopover"]')).toBeTruthy(); }); + + describe('UtilityBarAdditionalFiltersContent', () => { + test('does not show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is false', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be false + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(false); + }); + + test('does show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is true', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be true + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(true); + }); + + test('calls the onShowBuildingBlockAlertsChanged when the check box is clicked', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // check the box + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .simulate('change', { target: { checked: true } }); + + // Make sure our callback is called + expect(onShowBuildingBlockAlertsChanged).toHaveBeenCalled(); + }); + + test('can update showBuildingBlockAlerts from false to true', () => { + const Proxy = (props: AlertsUtilityBarProps) => ( + + + + ); + + const wrapper = mount( + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should false now since we initially set the showBuildingBlockAlerts to false + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(false); + + wrapper.setProps({ showBuildingBlockAlerts: true }); + wrapper.update(); + + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be true now since we changed the showBuildingBlockAlerts from false to true + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index bedc23790541c..bdad380f59ae9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -28,7 +28,7 @@ import { TimelineNonEcsData } from '../../../../graphql/types'; import { UpdateAlertsStatus } from '../types'; import { FILTER_CLOSED, FILTER_IN_PROGRESS, FILTER_OPEN } from '../alerts_filter_group'; -interface AlertsUtilityBarProps { +export interface AlertsUtilityBarProps { canUserCRUD: boolean; hasIndexWrite: boolean; areEventsLoading: boolean; @@ -223,5 +223,6 @@ export const AlertsUtilityBar = React.memo( prevProps.areEventsLoading === nextProps.areEventsLoading && prevProps.selectedEventIds === nextProps.selectedEventIds && prevProps.totalCount === nextProps.totalCount && - prevProps.showClearSelection === nextProps.showClearSelection + prevProps.showClearSelection === nextProps.showClearSelection && + prevProps.showBuildingBlockAlerts === nextProps.showBuildingBlockAlerts ); From d3f498af9aef9bda7f0ee51aa08b7faa728f317e Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 30 Jul 2020 22:37:54 -0500 Subject: [PATCH 20/55] [Metrics UI] Fix alert previews of ungrouped alerts (#73735) Co-authored-by: Elastic Machine --- .../alerting/metric_threshold/components/expression.tsx | 7 ++++++- .../metric_threshold/components/expression_chart.tsx | 2 +- .../hooks/use_metrics_explorer_chart_data.ts | 2 +- .../infra/public/alerting/metric_threshold/types.ts | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index cd1e93a2a0c96..f990578a471be 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -256,6 +256,11 @@ export const Expressions: React.FC = (props) => { [onFilterChange] ); + const groupByPreviewDisplayName = useMemo(() => { + if (Array.isArray(alertParams.groupBy)) return alertParams.groupBy.join(', '); + return alertParams.groupBy; + }, [alertParams.groupBy]); + return ( <> @@ -400,7 +405,7 @@ export const Expressions: React.FC = (props) => { showNoDataResults={alertParams.alertOnNoData} validate={validateMetricThreshold} fetch={alertsContext.http.fetch} - groupByDisplayName={alertParams.groupBy} + groupByDisplayName={groupByPreviewDisplayName} /> diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index cdb6b341c7299..c90c534193fdc 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -45,7 +45,7 @@ interface Props { derivedIndexPattern: IIndexPattern; source: InfraSource | null; filterQuery?: string; - groupBy?: string; + groupBy?: string | string[]; } const tooltipProps = { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts index 185895062cfe2..a3d09742e9a57 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -19,7 +19,7 @@ export const useMetricsExplorerChartData = ( derivedIndexPattern: IIndexPattern, source: InfraSource | null, filterQuery?: string, - groupBy?: string + groupBy?: string | string[] ) => { const { timeSize, timeUnit } = expression || { timeSize: 1, timeUnit: 'm' }; const options: MetricsExplorerOptions = useMemo( diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts index 58586c1dd8b98..b2317c558be44 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -53,7 +53,7 @@ export interface ExpressionChartData { export interface AlertParams { criteria: MetricExpression[]; - groupBy?: string; + groupBy?: string[]; filterQuery?: string; sourceId?: string; filterQueryText?: string; From 5fa162c69910d13ad771026235840056b1eaa6e0 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 30 Jul 2020 22:39:34 -0500 Subject: [PATCH 21/55] [Metrics UI] Fix all threshold alert conditions disappearing due to alert prefill (#73708) Co-authored-by: Elastic Machine --- .../alerting/metric_threshold/components/expression.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index f990578a471be..8bb8b3934b5fd 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -185,7 +185,7 @@ export const Expressions: React.FC = (props) => { const preFillAlertCriteria = useCallback(() => { const md = alertsContext.metadata; - if (md && md.currentOptions?.metrics) { + if (md?.currentOptions?.metrics?.length) { setAlertParams( 'criteria', md.currentOptions.metrics.map((metric) => ({ @@ -249,7 +249,7 @@ export const Expressions: React.FC = (props) => { if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id || 'default'); } - }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + }, [alertsContext.metadata, source]); // eslint-disable-line react-hooks/exhaustive-deps const handleFieldSearchChange = useCallback( (e: ChangeEvent) => onFilterChange(e.target.value), From 39cca0e55df05db3d165eb5c4756b4b37422972a Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 31 Jul 2020 06:52:36 +0200 Subject: [PATCH 22/55] [Discover] Improve saveSearch functional test handling (#73626) * Check for submit button to be disabled, before submitting the form to prevent occasional flakiness --- test/functional/page_objects/discover_page.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 8f69bf629ce28..c558d9e2d8a31 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -17,11 +17,9 @@ * under the License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function DiscoverPageProvider({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); const find = getService('find'); @@ -51,9 +49,16 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider } public async saveSearch(searchName: string) { - log.debug('saveSearch'); await this.clickSaveSearchButton(); - await testSubjects.setValue('savedObjectTitle', searchName); + // preventing an occasional flakiness when the saved object wasn't set and the form can't be submitted + await retry.waitFor( + `saved search title is set to ${searchName} and save button is clickable`, + async () => { + const saveButton = await testSubjects.find('confirmSaveSavedObjectButton'); + await testSubjects.setValue('savedObjectTitle', searchName); + return (await saveButton.getAttribute('disabled')) !== 'true'; + } + ); await testSubjects.click('confirmSaveSavedObjectButton'); await header.waitUntilLoadingHasFinished(); // LeeDr - this additional checking for the saved search name was an attempt @@ -61,9 +66,8 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider // that the next action wouldn't have to retry. But it doesn't really solve // that issue. But it does typically take about 3 retries to // complete with the expected searchName. - await retry.try(async () => { - const name = await this.getCurrentQueryName(); - expect(name).to.be(searchName); + await retry.waitFor(`saved search was persisted with name ${searchName}`, async () => { + return (await this.getCurrentQueryName()) === searchName; }); } @@ -96,11 +100,11 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider // We need this try loop here because previous actions in Discover like // saving a search cause reloading of the page and the "Open" menu item goes stale. - await retry.try(async () => { + await retry.waitFor('saved search panel is opened', async () => { await this.clickLoadSavedSearchButton(); await header.waitUntilLoadingHasFinished(); isOpen = await testSubjects.exists('loadSearchForm'); - expect(isOpen).to.be(true); + return isOpen === true; }); } From 145f2eef57ca5b1bb1956a28fdb48db2dad345d8 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 31 Jul 2020 10:25:01 +0200 Subject: [PATCH 23/55] [ML] Migrate to React BrowserRouter and Kibana provided History. (#71941) - Migrate to React BrowserRouter and Kibana provided History including a fallback to redirect legacy hash based URLs. - Migrate breadcrumbs away from hash based URLs. - Make sure relative custom urls still work after migration. --- x-pack/plugins/ml/public/application/app.tsx | 10 +- .../components/anomalies_table/links_menu.js | 6 +- .../application/contexts/kibana/index.ts | 1 + .../contexts/kibana/use_navigate_to_path.ts | 34 +++++++ .../back_to_list_panel/back_to_list_panel.tsx | 10 +- .../components/action_clone/clone_button.tsx | 8 +- .../action_view/get_view_action.tsx | 10 +- .../components/action_view/view_button.tsx | 18 +--- .../components/analytics_list/use_actions.tsx | 2 +- .../source_selection/source_selection.tsx | 13 +-- .../datavisualizer_selector.tsx | 7 +- .../components/custom_url_editor/list.tsx | 4 +- .../edit_job_flyout/tabs/custom_urls.tsx | 7 +- .../common/job_creator/util/general.ts | 41 +++----- .../calendars/calendars_selection.tsx | 12 ++- .../single_metric_view/settings.tsx | 10 +- .../pages/components/summary_step/summary.tsx | 11 ++- .../new_job/pages/index_or_search/page.tsx | 11 ++- .../jobs/new_job/pages/job_type/page.tsx | 30 +++--- .../public/application/routing/breadcrumbs.ts | 49 ++++++++-- .../ml/public/application/routing/router.tsx | 97 ++++++++++++++----- .../routing/routes/access_denied.tsx | 4 +- .../analytics_job_creation.tsx | 34 ++++--- .../analytics_job_exploration.tsx | 38 ++++---- .../analytics_jobs_list.tsx | 30 +++--- .../routes/datavisualizer/datavisualizer.tsx | 15 +-- .../routes/datavisualizer/file_based.tsx | 30 +++--- .../routes/datavisualizer/index_based.tsx | 36 +++---- .../application/routing/routes/explorer.tsx | 30 +++--- .../application/routing/routes/index.ts | 2 +- .../application/routing/routes/jobs_list.tsx | 31 +++--- .../routes/new_job/index_or_search.tsx | 28 +++--- .../routing/routes/new_job/job_type.tsx | 34 ++++--- .../routing/routes/new_job/new_job.tsx | 20 +--- .../routing/routes/new_job/recognize.tsx | 43 ++++---- .../routing/routes/new_job/wizard.tsx | 64 ++++++------ .../application/routing/routes/overview.tsx | 35 +++---- .../routing/routes/settings/calendar_list.tsx | 32 +++--- .../routes/settings/calendar_new_edit.tsx | 56 ++++++----- .../routing/routes/settings/filter_list.tsx | 30 +++--- .../routes/settings/filter_list_new_edit.tsx | 57 ++++++----- .../routing/routes/settings/settings.tsx | 15 +-- .../routing/routes/timeseriesexplorer.tsx | 12 ++- .../application/util/custom_url_utils.test.ts | 46 +++++++++ .../application/util/custom_url_utils.ts | 30 ++++-- .../plugins/ml/public/url_generator.test.ts | 2 +- x-pack/plugins/ml/public/url_generator.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 49 files changed, 669 insertions(+), 480 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index cf645404860f5..cc3af9d7f4980 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -25,6 +25,7 @@ export type MlDependencies = Omit & MlStartDepende interface AppProps { coreStart: CoreStart; deps: MlDependencies; + appMountParams: AppMountParameters; } const localStorage = new Storage(window.localStorage); @@ -46,8 +47,9 @@ export interface MlServicesContext { export type MlGlobalServices = ReturnType; -const App: FC = ({ coreStart, deps }) => { +const App: FC = ({ coreStart, deps, appMountParams }) => { const pageDeps = { + history: appMountParams.history, indexPatterns: deps.data.indexPatterns, config: coreStart.uiSettings!, setBreadcrumbs: coreStart.chrome!.setBreadcrumbs, @@ -104,7 +106,11 @@ export const renderApp = ( appMountParams.onAppLeave((actions) => actions.default()); const mlLicense = setLicenseCache(deps.licensing, [ - () => ReactDOM.render(, appMountParams.element), + () => + ReactDOM.render( + , + appMountParams.element + ), ]); return () => { diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 4850d583a626c..f603264896cd3 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -62,6 +62,8 @@ class LinksMenuUI extends Component { const timestamp = record.timestamp; const configuredUrlValue = customUrl.url_value; const timeRangeInterval = parseInterval(customUrl.time_range); + const basePath = this.props.kibana.services.http.basePath.get(); + if (configuredUrlValue.includes('$earliest$')) { let earliestMoment = moment(timestamp); if (timeRangeInterval !== null) { @@ -117,7 +119,7 @@ class LinksMenuUI extends Component { // Replace any tokens in the configured url_value with values from the source record, // and then open link in a new tab/window. const urlPath = replaceStringTokens(customUrl.url_value, record, true); - openCustomUrlWindow(urlPath, customUrl); + openCustomUrlWindow(urlPath, customUrl, basePath); }) .catch((resp) => { console.log('openCustomUrl(): error loading categoryDefinition:', resp); @@ -136,7 +138,7 @@ class LinksMenuUI extends Component { // Replace any tokens in the configured url_value with values from the source record, // and then open link in a new tab/window. const urlPath = getUrlForRecord(customUrl, record); - openCustomUrlWindow(urlPath, customUrl); + openCustomUrlWindow(urlPath, customUrl, basePath); } }; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts index 0f071a42a5688..8a43ae12deb21 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts @@ -5,6 +5,7 @@ */ export { useMlKibana, StartServices, MlKibanaReactContextValue } from './kibana_context'; +export { useNavigateToPath, NavigateToPath } from './use_navigate_to_path'; export { useUiSettings } from './use_ui_settings_context'; export { useTimefilter } from './use_timefilter'; export { useNotifications } from './use_notifications_context'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts new file mode 100644 index 0000000000000..f2db970bf5057 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; +import { PLUGIN_ID } from '../../../../common/constants/app'; + +import { useMlKibana } from './kibana_context'; + +export type NavigateToPath = ReturnType; + +export const useNavigateToPath = () => { + const { + services: { + application: { getUrlForApp, navigateToUrl }, + }, + } = useMlKibana(); + + const location = useLocation(); + + return useMemo( + () => (path: string | undefined, preserveSearch = false) => { + navigateToUrl( + getUrlForApp(PLUGIN_ID, { + path: `${path}${preserveSearch === true ? location.search : ''}`, + }) + ); + }, + [location] + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx index b6b335afa53f5..183cbe084f9b3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx @@ -7,17 +7,13 @@ import React, { FC, Fragment } from 'react'; import { EuiCard, EuiHorizontalRule, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; export const BackToListPanel: FC = () => { - const { - services: { - application: { navigateToUrl }, - }, - } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const redirectToAnalyticsManagementPage = async () => { - await navigateToUrl('#/data_frame_analytics?'); + await navigateToPath('/data_frame_analytics'); }; return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx index 7a409e5238a57..010aa7b8513b5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx @@ -13,7 +13,7 @@ import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { CreateAnalyticsFormProps, DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, @@ -350,11 +350,11 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { export const useNavigateToWizardWithClonedJob = () => { const { services: { - application: { navigateToUrl }, notifications: { toasts }, savedObjects, }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const savedObjectsClient = savedObjects.client; @@ -395,8 +395,8 @@ export const useNavigateToWizardWithClonedJob = () => { } if (sourceIndexId) { - await navigateToUrl( - `ml#/data_frame_analytics/new_job?index=${encodeURIComponent(sourceIndexId)}&jobId=${ + await navigateToPath( + `/data_frame_analytics/new_job?index=${encodeURIComponent(sourceIndexId)}&jobId=${ item.config.id }` ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx index e31670ea42ceb..e123af204b515 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx @@ -12,11 +12,9 @@ import { DataFrameAnalyticsListRow } from '../analytics_list/common'; import { ViewButton } from './view_button'; -export const getViewAction = ( - isManagementTable: boolean = false -): EuiTableActionsColumnType['actions'][number] => ({ +export const getViewAction = (): EuiTableActionsColumnType< + DataFrameAnalyticsListRow +>['actions'][number] => ({ isPrimary: true, - render: (item: DataFrameAnalyticsListRow) => ( - - ), + render: (item: DataFrameAnalyticsListRow) => , }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx index a0790cd802409..9472a3af852fc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { getAnalysisType } from '../../../../common/analytics'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; import { getResultsUrl, DataFrameAnalyticsListRow } from '../analytics_list/common'; @@ -17,23 +17,15 @@ import { getViewLinkStatus } from './get_view_link_status'; interface ViewButtonProps { item: DataFrameAnalyticsListRow; - isManagementTable: boolean; } -export const ViewButton: FC = ({ item, isManagementTable }) => { - const { - services: { - application: { navigateToUrl, navigateToApp }, - }, - } = useMlKibana(); +export const ViewButton: FC = ({ item }) => { + const navigateToPath = useNavigateToPath(); const { disabled, tooltipContent } = getViewLinkStatus(item); const analysisType = getAnalysisType(item.config.analysis); - const url = getResultsUrl(item.id, analysisType); - const navigator = isManagementTable - ? () => navigateToApp('ml', { path: url }) - : () => navigateToUrl(url); + const onClickHandler = () => navigateToPath(getResultsUrl(item.id, analysisType)); const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', { defaultMessage: 'View', @@ -47,7 +39,7 @@ export const ViewButton: FC = ({ item, isManagementTable }) => flush="left" iconType="visTable" isDisabled={disabled} - onClick={navigator} + onClick={onClickHandler} size="xs" > {buttonText} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx index bc02c81bac0f0..373b9991d4d3c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx @@ -43,7 +43,7 @@ export const useActions = ( let modals: JSX.Element | null = null; const actions: EuiTableActionsColumnType['actions'] = [ - getViewAction(isManagementTable), + getViewAction(), ]; // isManagementTable will be the same for the lifecycle of the component diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index b03a58a02309d..29d495062e309 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; const fixedPageSize: number = 8; @@ -27,16 +27,13 @@ interface Props { export const SourceSelection: FC = ({ onClose }) => { const { - services: { - application: { navigateToUrl }, - savedObjects, - uiSettings, - }, + services: { savedObjects, uiSettings }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const onSearchSelected = async (id: string, type: string) => { - await navigateToUrl( - `ml#/data_frame_analytics/new_job?${ + await navigateToPath( + `/data_frame_analytics/new_job?${ type === 'index-pattern' ? 'index' : 'savedSearchId' }=${encodeURIComponent(id)}` ); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index fd86d9f48f46d..769b83c03110b 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { isFullLicense } from '../license'; -import { useTimefilter, useMlKibana } from '../contexts/kibana'; +import { useTimefilter, useMlKibana, useNavigateToPath } from '../contexts/kibana'; import { NavigationMenu } from '../components/navigation_menu'; import { getMaxBytesFormatted } from './file_based/components/utils'; @@ -54,6 +54,7 @@ export const DatavisualizerSelector: FC = () => { const { services: { licenseManagement }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const startTrialVisible = licenseManagement !== undefined && @@ -124,7 +125,7 @@ export const DatavisualizerSelector: FC = () => { footer={ navigateToPath('/filedatavisualizer')} data-test-subj="mlDataVisualizerUploadFileButton" > { footer={ navigateToPath('/datavisualizer_index_select')} data-test-subj="mlDataVisualizerSelectIndexButton" > = ({ job, customUrls, setCustomUrls }) => { const { - services: { notifications }, + services: { http, notifications }, } = useMlKibana(); const [expandedUrlIndex, setExpandedUrlIndex] = useState(null); @@ -103,7 +103,7 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust if (index < customUrls.length) { getTestUrl(job, customUrls[index]) .then((testUrl) => { - openCustomUrlWindow(testUrl, customUrls[index]); + openCustomUrlWindow(testUrl, customUrls[index], http.basePath.get()); }) .catch((resp) => { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx index 7af27fc22e34c..468efcf013e9b 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx @@ -160,13 +160,16 @@ class CustomUrlsUI extends Component { }; onTestButtonClick = () => { - const { toasts } = this.props.kibana.services.notifications; + const { + http: { basePath }, + notifications: { toasts }, + } = this.props.kibana.services; const job = this.props.job; buildCustomUrlFromSettings(this.state.editorSettings as CustomUrlSettings) .then((customUrl) => { getTestUrl(job, customUrl) .then((testUrl) => { - openCustomUrlWindow(testUrl, customUrl); + openCustomUrlWindow(testUrl, customUrl, basePath.get()); }) .catch((resp) => { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 92e65e580fc01..8ab45dc24aa17 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -5,8 +5,10 @@ */ import { i18n } from '@kbn/i18n'; + import { Job, Datafeed, Detector } from '../../../../../../../common/types/anomaly_detection_jobs'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { NavigateToPath } from '../../../../../contexts/kibana'; import { ML_JOB_AGGREGATION, SPARSE_DATA_AGGREGATIONS, @@ -20,12 +22,7 @@ import { mlCategory, } from '../../../../../../../common/types/fields'; import { mlJobService } from '../../../../../services/job_service'; -import { - JobCreatorType, - isMultiMetricJobCreator, - isPopulationJobCreator, - isCategorizationJobCreator, -} from '../index'; +import { JobCreatorType } from '../index'; import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../../common/constants/new_job'; const getFieldByIdFactory = (additionalFields: Field[]) => (id: string) => { @@ -247,43 +244,33 @@ function stashCombinedJob( mlJobService.tempJobCloningObjects.calendars = jobCreator.calendars; } -export function convertToMultiMetricJob(jobCreator: JobCreatorType) { +export function convertToMultiMetricJob( + jobCreator: JobCreatorType, + navigateToPath: NavigateToPath +) { jobCreator.createdBy = CREATED_BY_LABEL.MULTI_METRIC; jobCreator.modelPlot = false; stashCombinedJob(jobCreator, true, true); - window.location.href = window.location.href.replace( - JOB_TYPE.SINGLE_METRIC, - JOB_TYPE.MULTI_METRIC - ); + navigateToPath(`jobs/new_job/${JOB_TYPE.MULTI_METRIC}`, true); } -export function convertToAdvancedJob(jobCreator: JobCreatorType) { +export function convertToAdvancedJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.createdBy = null; stashCombinedJob(jobCreator, true, true); - let jobType = JOB_TYPE.SINGLE_METRIC; - if (isMultiMetricJobCreator(jobCreator)) { - jobType = JOB_TYPE.MULTI_METRIC; - } else if (isPopulationJobCreator(jobCreator)) { - jobType = JOB_TYPE.POPULATION; - } else if (isCategorizationJobCreator(jobCreator)) { - jobType = JOB_TYPE.CATEGORIZATION; - } - - window.location.href = window.location.href.replace(jobType, JOB_TYPE.ADVANCED); + navigateToPath(`jobs/new_job/${JOB_TYPE.ADVANCED}`, true); } -export function resetJob(jobCreator: JobCreatorType) { +export function resetJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.jobId = ''; stashCombinedJob(jobCreator, true, true); - - window.location.href = '#/jobs/new_job'; + navigateToPath('/jobs/new_job'); } -export function advancedStartDatafeed(jobCreator: JobCreatorType) { +export function advancedStartDatafeed(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { stashCombinedJob(jobCreator, false, false); - window.location.href = '#/jobs'; + navigateToPath('/jobs'); } export function aggFieldPairsCanBeCharted(afs: AggFieldPair[]) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx index 60b034b516939..7999ce46bc9ed 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx @@ -22,10 +22,18 @@ import { i18n } from '@kbn/i18n'; import { JobCreatorContext } from '../../../../../job_creator_context'; import { Description } from './description'; import { ml } from '../../../../../../../../../services/ml_api_service'; +import { PLUGIN_ID } from '../../../../../../../../../../../common/constants/app'; import { Calendar } from '../../../../../../../../../../../common/types/calendars'; +import { useMlKibana } from '../../../../../../../../../contexts/kibana'; import { GLOBAL_CALENDAR } from '../../../../../../../../../../../common/constants/calendars'; export const CalendarsSelection: FC = () => { + const { + services: { + application: { getUrlForApp }, + }, + } = useMlKibana(); + const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); const [selectedCalendars, setSelectedCalendars] = useState(jobCreator.calendars); const [selectedOptions, setSelectedOptions] = useState>>( @@ -64,7 +72,9 @@ export const CalendarsSelection: FC = () => { }, }; - const manageCalendarsHref = '#/settings/calendars_list'; + const manageCalendarsHref = getUrlForApp(PLUGIN_ID, { + path: '/settings/calendars_list', + }); return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx index 3bcac1cf6876c..e14e29cc965d3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx @@ -8,21 +8,25 @@ import React, { Fragment, FC, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { useNavigateToPath } from '../../../../../../../contexts/kibana'; + +import { convertToMultiMetricJob } from '../../../../../common/job_creator/util/general'; + import { JobCreatorContext } from '../../../job_creator_context'; + import { BucketSpan } from '../bucket_span'; import { SparseDataSwitch } from '../sparse_data'; -import { convertToMultiMetricJob } from '../../../../../common/job_creator/util/general'; - interface Props { setIsValid: (proceed: boolean) => void; } export const SingleMetricSettings: FC = ({ setIsValid }) => { const { jobCreator } = useContext(JobCreatorContext); + const navigateToPath = useNavigateToPath(); const convertToMultiMetric = () => { - convertToMultiMetricJob(jobCreator); + convertToMultiMetricJob(jobCreator, navigateToPath); }; return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index d8cd0f5e4f1f0..5ef59951c43cc 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { PreviousButton } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; @@ -42,6 +42,9 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => const { services: { notifications }, } = useMlKibana(); + + const navigateToPath = useNavigateToPath(); + const { jobCreator, jobValidator, jobValidatorUpdated, resultsLoader } = useContext( JobCreatorContext ); @@ -87,7 +90,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => try { await jobCreator.createJob(); await jobCreator.createDatafeed(); - advancedStartDatafeed(jobCreator); + advancedStartDatafeed(jobCreator, navigateToPath); } catch (error) { // catch and display all job creation errors const { toasts } = notifications; @@ -112,11 +115,11 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => } function clickResetJob() { - resetJob(jobCreator); + resetJob(jobCreator, navigateToPath); } const convertToAdvanced = () => { - convertToAdvancedJob(jobCreator); + convertToAdvancedJob(jobCreator, navigateToPath); }; useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx index 0f990a07aaf21..0caf97b0006d4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx @@ -16,7 +16,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { SavedObjectFinderUi } from '../../../../../../../../../src/plugins/saved_objects/public'; -import { useMlKibana } from '../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../contexts/kibana'; export interface PageProps { nextStepPath: string; @@ -25,11 +25,14 @@ export interface PageProps { export const Page: FC = ({ nextStepPath }) => { const RESULTS_PER_PAGE = 20; const { uiSettings, savedObjects } = useMlKibana().services; + const navigateToPath = useNavigateToPath(); const onObjectSelection = (id: string, type: string) => { - window.location.href = `${nextStepPath}?${ - type === 'index-pattern' ? 'index' : 'savedSearchId' - }=${encodeURIComponent(id)}`; + navigateToPath( + `${nextStepPath}?${type === 'index-pattern' ? 'index' : 'savedSearchId'}=${encodeURIComponent( + id + )}` + ); }; return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index 3bfe0569e75be..be0135ec3f1e0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -18,6 +18,7 @@ import { EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useNavigateToPath } from '../../../../contexts/kibana'; import { useMlContext } from '../../../../contexts/ml'; import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { DataRecognizer } from '../../../../components/data_recognizer'; @@ -28,6 +29,8 @@ import { CategorizationIcon } from './categorization_job_icon'; export const Page: FC = () => { const mlContext = useMlContext(); + const navigateToPath = useNavigateToPath(); + const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); const { currentSavedSearch, currentIndexPattern } = mlContext; @@ -68,25 +71,23 @@ export const Page: FC = () => { }, }; - const getUrl = (basePath: string) => { + const getUrlParams = () => { return !isSavedSearchSavedObject(currentSavedSearch) - ? `${basePath}?index=${currentIndexPattern.id}` - : `${basePath}?savedSearchId=${currentSavedSearch.id}`; + ? `?index=${currentIndexPattern.id}` + : `?savedSearchId=${currentSavedSearch.id}`; }; const addSelectionToRecentlyAccessed = () => { const title = !isSavedSearchSavedObject(currentSavedSearch) ? currentIndexPattern.title : (currentSavedSearch.attributes.title as string); - const url = getUrl(''); - addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, url); - - window.location.href = getUrl('#jobs/new_job/datavisualizer'); + addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, ''); + navigateToPath(`/jobs/new_job/datavisualizer${getUrlParams()}`); }; const jobTypes = [ { - href: getUrl('#jobs/new_job/single_metric'), + onClick: () => navigateToPath(`/jobs/new_job/single_metric${getUrlParams()}`), icon: { type: 'createSingleMetricJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.singleMetricAriaLabel', { @@ -102,7 +103,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkSingleMetricJob', }, { - href: getUrl('#jobs/new_job/multi_metric'), + onClick: () => navigateToPath(`/jobs/new_job/multi_metric${getUrlParams()}`), icon: { type: 'createMultiMetricJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.multiMetricAriaLabel', { @@ -119,7 +120,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkMultiMetricJob', }, { - href: getUrl('#jobs/new_job/population'), + onClick: () => navigateToPath(`/jobs/new_job/population${getUrlParams()}`), icon: { type: 'createPopulationJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.populationAriaLabel', { @@ -136,7 +137,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkPopulationJob', }, { - href: getUrl('#jobs/new_job/advanced'), + onClick: () => navigateToPath(`/jobs/new_job/advanced${getUrlParams()}`), icon: { type: 'createAdvancedJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.advancedAriaLabel', { @@ -153,7 +154,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkAdvancedJob', }, { - href: getUrl('#jobs/new_job/categorization'), + onClick: () => navigateToPath(`/jobs/new_job/categorization${getUrlParams()}`), icon: { type: CategorizationIcon, ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.categorizationAriaLabel', { @@ -247,11 +248,11 @@ export const Page: FC = () => { - {jobTypes.map(({ href, icon, title, description, id }) => ( + {jobTypes.map(({ onClick, icon, title, description, id }) => ( { /> } onClick={addSelectionToRecentlyAccessed} - href={getUrl('#jobs/new_job/datavisualizer')} /> diff --git a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts index 82e233745f9e4..d0a4f999af758 100644 --- a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts +++ b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts @@ -4,47 +4,82 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiBreadcrumb } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; + import { ChromeBreadcrumb } from 'kibana/public'; +import { NavigateToPath } from '../contexts/kibana'; + export const ML_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.machineLearningBreadcrumbLabel', { defaultMessage: 'Machine Learning', }), - href: '#/', + href: '/', }); -export const SETTINGS: ChromeBreadcrumb = Object.freeze({ +export const SETTINGS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.settingsBreadcrumbLabel', { defaultMessage: 'Settings', }), - href: '#/settings', + href: '/settings', }); export const ANOMALY_DETECTION_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.anomalyDetectionBreadcrumbLabel', { defaultMessage: 'Anomaly Detection', }), - href: '#/jobs', + href: '/jobs', }); export const DATA_FRAME_ANALYTICS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.dataFrameAnalyticsLabel', { defaultMessage: 'Data Frame Analytics', }), - href: '#/data_frame_analytics', + href: '/data_frame_analytics', }); export const DATA_VISUALIZER_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.datavisualizerBreadcrumbLabel', { defaultMessage: 'Data Visualizer', }), - href: '#/datavisualizer', + href: '/datavisualizer', }); export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.createJobsBreadcrumbLabel', { defaultMessage: 'Create job', }), - href: '#/jobs/new_job', + href: '/jobs/new_job', }); + +const breadcrumbs = { + ML_BREADCRUMB, + SETTINGS_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + DATA_FRAME_ANALYTICS_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + CREATE_JOB_BREADCRUMB, +}; +type Breadcrumb = keyof typeof breadcrumbs; + +export const breadcrumbOnClickFactory = ( + path: string | undefined, + navigateToPath: NavigateToPath +): EuiBreadcrumb['onClick'] => { + return (e) => { + e.preventDefault(); + navigateToPath(path); + }; +}; + +export const getBreadcrumbWithUrlForApp = ( + breadcrumbName: Breadcrumb, + navigateToPath: NavigateToPath +): EuiBreadcrumb => { + return { + ...breadcrumbs[breadcrumbName], + onClick: breadcrumbOnClickFactory(breadcrumbs[breadcrumbName].href, navigateToPath), + }; +}; diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index f1b8083f19ccf..56c9a19723fba 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; -import { HashRouter, Route, RouteProps } from 'react-router-dom'; +import React, { useEffect, FC } from 'react'; +import { useHistory, useLocation, Router, Route, RouteProps } from 'react-router-dom'; import { Location } from 'history'; -import { IUiSettingsClient, ChromeStart } from 'kibana/public'; +import { AppMountParameters, IUiSettingsClient, ChromeStart } from 'kibana/public'; import { ChromeBreadcrumb } from 'kibana/public'; import { IndexPatternsContract } from 'src/plugins/data/public'; + +import { useNavigateToPath } from '../contexts/kibana'; import { MlContext, MlContextValue } from '../contexts/ml'; import { UrlStateProvider } from '../util/url_state'; @@ -33,9 +35,10 @@ export interface PageProps { } interface PageDependencies { - setBreadcrumbs: ChromeStart['setBreadcrumbs']; - indexPatterns: IndexPatternsContract; config: IUiSettingsClient; + history: AppMountParameters['history']; + indexPatterns: IndexPatternsContract; + setBreadcrumbs: ChromeStart['setBreadcrumbs']; } export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children }) => { @@ -44,28 +47,74 @@ export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children ); }; -export const MlRouter: FC<{ pageDeps: PageDependencies }> = ({ pageDeps }) => { - const setBreadcrumbs = pageDeps.setBreadcrumbs; +/** + * This component provides compatibility with the previous hash based + * URL format used by HashRouter. Even if we migrate all internal URLs + * to one without hashes, we should keep this redirect in place to + * support legacy bookmarks and as a fallback for unmigrated URLs + * from other plugins. + */ +const LegacyHashUrlRedirect: FC = ({ children }) => { + const history = useHistory(); + const location = useLocation(); + + useEffect(() => { + if (location.hash.startsWith('#/')) { + history.push(location.hash.replace('#', '')); + } + }, [location.hash]); + + return <>{children}; +}; + +/** + * `MlRoutes` creates a React Router Route for every routeFactory + * and passes on the `navigateToPath` helper. + */ +const MlRoutes: FC<{ + pageDeps: PageDependencies; +}> = ({ pageDeps }) => { + const navigateToPath = useNavigateToPath(); return ( - + <> + {Object.entries(routes).map(([name, routeFactory]) => { + const route = routeFactory(navigateToPath); + + return ( + { + window.setTimeout(() => { + pageDeps.setBreadcrumbs(route.breadcrumbs); + }); + return route.render(props, pageDeps); + }} + /> + ); + })} + + ); +}; + +/** + * `MlRouter` is based on `BrowserRouter` and takes in `ScopedHistory` provided + * by Kibana. `LegacyHashUrlRedirect` provides compatibility with legacy hash based URLs. + * `UrlStateProvider` manages state stored in `_g/_a` URL parameters which can be + * use in components further down via `useUrlState()`. + */ +export const MlRouter: FC<{ + pageDeps: PageDependencies; +}> = ({ pageDeps }) => ( + +
- {Object.entries(routes).map(([name, route]) => ( - { - window.setTimeout(() => { - setBreadcrumbs(route.breadcrumbs); - }); - return route.render(props, pageDeps); - }} - /> - ))} +
-
- ); -}; + + +); diff --git a/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx b/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx index bd7fc434b36ac..42d9a59d15bfa 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx @@ -19,11 +19,11 @@ const breadcrumbs = [ }, ]; -export const accessDeniedRoute: MlRoute = { +export const accessDeniedRouteFactory = (): MlRoute => ({ path: '/access-denied', render: (props, deps) => , breadcrumbs, -}; +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, {}); diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx index ebc7bd95fb0c3..8c45398098b2f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx @@ -5,29 +5,31 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { parse } from 'query-string'; + +import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_creation'; -import { ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { - defaultMessage: 'Data Frame Analytics', - }), - href: '#/data_frame_analytics', - }, -]; - -export const analyticsJobsCreationRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const analyticsJobsCreationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics/new_job', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { + defaultMessage: 'Data Frame Analytics', + }), + onClick: breadcrumbOnClickFactory('/data_frame_analytics', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, jobId, savedSearchId }: Record = parse(location.search, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index 1ffea2c06faf4..47cc002ab4d83 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -4,33 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; +import { parse } from 'query-string'; import { decode } from 'rison-node'; + +import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'; import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics'; -import { ML_BREADCRUMB, DATA_FRAME_ANALYTICS_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_FRAME_ANALYTICS_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { - defaultMessage: 'Exploration', - }), - href: '', - }, -]; - -export const analyticsJobExplorationRoute: MlRoute = { +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const analyticsJobExplorationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics/exploration', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { + defaultMessage: 'Exploration', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx index 2623136d1e98f..b6ef9ea81b4ba 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -7,28 +7,28 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_management'; -import { ML_BREADCRUMB, DATA_FRAME_ANALYTICS_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_FRAME_ANALYTICS_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { - defaultMessage: 'Job Management', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const analyticsJobsListRoute: MlRoute = { +export const analyticsJobsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { + defaultMessage: 'Job Management', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index fc2d517b2edb1..efe5c3cba04a5 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -11,21 +11,24 @@ import React, { FC } from 'react'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { DatavisualizerSelector } from '../../../datavisualizer'; import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const selectorRoute: MlRoute = { +export const selectorRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/datavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 1115a38870821..485af52c45a55 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { FileDataVisualizerPage } from '../../../datavisualizer/file_based'; @@ -20,24 +22,22 @@ import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { - defaultMessage: 'File', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const fileBasedRoute: MlRoute = { +export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/filedatavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { + defaultMessage: 'File', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index 1ec73fced82fe..358b8773e3460 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; +import { parse } from 'query-string'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { Page } from '../../../datavisualizer/index_based'; @@ -15,24 +19,22 @@ import { checkBasicLicense } from '../../../license'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { - defaultMessage: 'Index', - }), - href: '', - }, -]; - -export const indexBasedRoute: MlRoute = { +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/datavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { + defaultMessage: 'Index', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 7d09797a0ff1b..a2030776773a9 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -9,6 +9,8 @@ import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../contexts/kibana'; + import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { MlRoute, PageLoader, PageProps } from '../router'; @@ -27,26 +29,24 @@ import { useShowCharts } from '../../components/controls/checkbox_showcharts'; import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; import { useUrlState } from '../../util/url_state'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; import { isViewBySwimLaneData } from '../../explorer/swimlane_container'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { - defaultMessage: 'Anomaly Explorer', - }), - href: '', - }, -]; - -export const explorerRoute: MlRoute = { +export const explorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/explorer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { + defaultMessage: 'Anomaly Explorer', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context, results } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/index.ts b/x-pack/plugins/ml/public/application/routing/routes/index.ts index 44111ae32cd30..fe7ecd129ebef 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/index.ts +++ b/x-pack/plugins/ml/public/application/routing/routes/index.ts @@ -10,6 +10,6 @@ export * from './new_job'; export * from './datavisualizer'; export * from './settings'; export * from './data_frame_analytics'; -export { timeSeriesExplorerRoute } from './timeseriesexplorer'; +export { timeSeriesExplorerRouteFactory } from './timeseriesexplorer'; export * from './explorer'; export * from './access_denied'; diff --git a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx index c1d686d356dda..db58b6a537e06 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -7,6 +7,9 @@ import React, { useEffect, FC } from 'react'; import { useObservable } from 'react-use'; import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../contexts/kibana'; + import { DEFAULT_REFRESH_INTERVAL_MS } from '../../../../common/constants/jobs_list'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; import { useUrlState } from '../../util/url_state'; @@ -15,24 +18,22 @@ import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; import { JobsPage } from '../../jobs/jobs_list'; import { useTimefilter } from '../../contexts/kibana'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { - defaultMessage: 'Job Management', - }), - href: '', - }, -]; - -export const jobListRoute: MlRoute = { +export const jobListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { + defaultMessage: 'Job Management', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index b630b09b1a46d..d8605c4cc9115 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -5,12 +5,16 @@ */ import React, { FC } from 'react'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/index_or_search'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; @@ -26,9 +30,9 @@ interface IndexOrSearchPageProps extends PageProps { mode: MODE; } -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, +const getBreadcrumbs = (navigateToPath: NavigateToPath) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { defaultMessage: 'Create job', @@ -37,31 +41,31 @@ const breadcrumbs = [ }, ]; -export const indexOrSearchRoute: MlRoute = { +export const indexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/step/index_or_search', render: (props, deps) => ( ), - breadcrumbs, -}; + breadcrumbs: getBreadcrumbs(navigateToPath), +}); -export const dataVizIndexOrSearchRoute: MlRoute = { +export const dataVizIndexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/datavisualizer_index_select', render: (props, deps) => ( ), - breadcrumbs, -}; + breadcrumbs: getBreadcrumbs(navigateToPath), +}); const PageWrapper: FC = ({ nextStepPath, deps, mode }) => { const newJobResolvers = { diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx index f0a25d880a082..b8ab29d40fa1f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -4,31 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; +import { parse } from 'query-string'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../jobs/new_job/pages/job_type'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { - defaultMessage: 'Create job', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const jobTypeRoute: MlRoute = { +export const jobTypeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/step/job_type', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { + defaultMessage: 'Create job', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx index b110434f6f0a8..b230da44c8d6d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx @@ -5,28 +5,16 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; import { MlRoute } from '../../router'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.jobWizardLabel', { - defaultMessage: 'Create job', - }), - href: '#/jobs/new_job', - }, -]; - -export const newJobRoute: MlRoute = { +export const newJobRouteFactory = (): MlRoute => ({ path: '/jobs/new_job', render: () => , - breadcrumbs, -}; + // no breadcrumbs since it's just a redirect + breadcrumbs: [], +}); const Page: FC = () => { return ; diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index 2cd40cbcd95e6..6be58828ee1a5 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -6,42 +6,41 @@ import { parse } from 'query-string'; import React, { FC } from 'react'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../jobs/new_job/recognize'; import { checkViewOrCreateJobs } from '../../../jobs/new_job/recognize/resolvers'; import { mlJobService } from '../../../services/job_service'; -import { - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - ML_BREADCRUMB, -} from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { - defaultMessage: 'Recognized index', - }), - href: '', - }, -]; - -export const recognizeRoute: MlRoute = { +export const recognizeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/recognize', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { + defaultMessage: 'Recognized index', + }), + href: '', + }, + ], +}); -export const checkViewOrCreateRoute: MlRoute = { +export const checkViewOrCreateRouteFactory = (): MlRoute => ({ path: '/modules/check_view_or_create', render: (props, deps) => , + // no breadcrumbs since it's just a redirect breadcrumbs: [], -}; +}); const PageWrapper: FC = ({ location, deps }) => { const { id, index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 14df9a1d44a85..35085fd557577 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -8,6 +8,8 @@ import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { basicResolvers } from '../../resolvers'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -16,20 +18,20 @@ import { JOB_TYPE } from '../../../../../common/constants/new_job'; import { mlJobService } from '../../../services/job_service'; import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; -import { - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - ML_BREADCRUMB, -} from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; interface WizardPageProps extends PageProps { jobType: JOB_TYPE; } -const baseBreadcrumbs = [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, CREATE_JOB_BREADCRUMB]; +const getBaseBreadcrumbs = (navigateToPath: NavigateToPath) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), +]; -const singleMetricBreadcrumbs = [ - ...baseBreadcrumbs, +const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { defaultMessage: 'Single metric', @@ -38,8 +40,8 @@ const singleMetricBreadcrumbs = [ }, ]; -const multiMetricBreadcrumbs = [ - ...baseBreadcrumbs, +const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { defaultMessage: 'Multi-metric', @@ -48,8 +50,8 @@ const multiMetricBreadcrumbs = [ }, ]; -const populationBreadcrumbs = [ - ...baseBreadcrumbs, +const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { defaultMessage: 'Population', @@ -58,8 +60,8 @@ const populationBreadcrumbs = [ }, ]; -const advancedBreadcrumbs = [ - ...baseBreadcrumbs, +const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { defaultMessage: 'Advanced configuration', @@ -68,8 +70,8 @@ const advancedBreadcrumbs = [ }, ]; -const categorizationBreadcrumbs = [ - ...baseBreadcrumbs, +const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.categorizationLabel', { defaultMessage: 'Categorization', @@ -78,35 +80,35 @@ const categorizationBreadcrumbs = [ }, ]; -export const singleMetricRoute: MlRoute = { +export const singleMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/single_metric', render: (props, deps) => , - breadcrumbs: singleMetricBreadcrumbs, -}; + breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath), +}); -export const multiMetricRoute: MlRoute = { +export const multiMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/multi_metric', render: (props, deps) => , - breadcrumbs: multiMetricBreadcrumbs, -}; + breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath), +}); -export const populationRoute: MlRoute = { +export const populationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/population', render: (props, deps) => , - breadcrumbs: populationBreadcrumbs, -}; + breadcrumbs: getPopulationBreadcrumbs(navigateToPath), +}); -export const advancedRoute: MlRoute = { +export const advancedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/advanced', render: (props, deps) => , - breadcrumbs: advancedBreadcrumbs, -}; + breadcrumbs: getAdvancedBreadcrumbs(navigateToPath), +}); -export const categorizationRoute: MlRoute = { +export const categorizationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/categorization', render: (props, deps) => , - breadcrumbs: categorizationBreadcrumbs, -}; + breadcrumbs: getCategorizationBreadcrumbs(navigateToPath), +}); const PageWrapper: FC = ({ location, jobType, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx index 9b08bbf35c448..174e9804b9689 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx @@ -8,6 +8,9 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; + +import { NavigateToPath } from '../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../router'; import { useResolver } from '../use_resolver'; import { OverviewPage } from '../../overview'; @@ -17,23 +20,21 @@ import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capab import { getMlNodeCount } from '../../ml_nodes_check'; import { loadMlServerInfo } from '../../services/ml_server_info'; import { useTimefilter } from '../../contexts/kibana'; -import { ML_BREADCRUMB } from '../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.overview.overviewLabel', { - defaultMessage: 'Overview', - }), - href: '#/overview', - }, -]; - -export const overviewRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../breadcrumbs'; + +export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/overview', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.overview.overviewLabel', { + defaultMessage: 'Overview', + }), + onClick: breadcrumbOnClickFactory('/overview', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { @@ -51,11 +52,11 @@ const PageWrapper: FC = ({ deps }) => { ); }; -export const appRootRoute: MlRoute = { +export const appRootRouteFactory = (): MlRoute => ({ path: '/', render: () => , breadcrumbs: [], -}; +}); const Page: FC = () => { return ; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index e015a3292acc4..f2ae57f1ec961 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,24 +25,22 @@ import { } from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { CalendarsList } from '../../../settings/calendars'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { - defaultMessage: 'Calendar management', - }), - href: '#/settings/calendars_list', - }, -]; - -export const calendarListRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const calendarListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { + defaultMessage: 'Calendar management', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index ebd58120853a9..a5c30e1eaaacc 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,7 +25,7 @@ import { } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { NewCalendar } from '../../../settings/calendars'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; enum MODE { NEW, @@ -34,39 +36,35 @@ interface NewCalendarPageProps extends PageProps { mode: MODE; } -const newBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/calendars_list/new_calendar', - }, -]; - -const editBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/calendars_list/edit_calendar', - }, -]; - -export const newCalendarRoute: MlRoute = { +export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list/new_calendar', render: (props, deps) => , - breadcrumbs: newBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { + defaultMessage: 'Create', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list/new_calendar', navigateToPath), + }, + ], +}); -export const editCalendarRoute: MlRoute = { +export const editCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list/edit_calendar/:calendarId', render: (props, deps) => , - breadcrumbs: editBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { + defaultMessage: 'Edit', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list/edit_calendar', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, mode, deps }) => { let calendarId: string | undefined; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index 25bded1a52db1..d734e18d72bab 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -24,24 +26,22 @@ import { import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { FilterLists } from '../../../settings/filter_lists'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { - defaultMessage: 'Filter lists', - }), - href: '#/settings/filter_lists', - }, -]; +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const filterListRoute: MlRoute = { +export const filterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { + defaultMessage: 'Filter lists', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 2f4ccecf2f1a2..c6f17bc7f6f68 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,7 +25,8 @@ import { } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { EditFilterList } from '../../../settings/filter_lists'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; enum MODE { NEW, @@ -34,39 +37,35 @@ interface NewFilterPageProps extends PageProps { mode: MODE; } -const newBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/filter_lists/new', - }, -]; - -const editBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/filter_lists/edit', - }, -]; - -export const newFilterListRoute: MlRoute = { +export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists/new_filter_list', render: (props, deps) => , - breadcrumbs: newBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { + defaultMessage: 'Create', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists/new', navigateToPath), + }, + ], +}); -export const editFilterListRoute: MlRoute = { +export const editFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists/edit_filter_list/:filterId', render: (props, deps) => , - breadcrumbs: editBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { + defaultMessage: 'Edit', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists/edit', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, mode, deps }) => { let filterId: string | undefined; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx index a80c173dbca34..3f4b269851469 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -11,6 +11,8 @@ import React, { FC } from 'react'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -22,15 +24,16 @@ import { } from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { AnomalyDetectionSettingsContext, Settings } from '../../../settings'; -import { ML_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; - -const breadcrumbs = [ML_BREADCRUMB, SETTINGS]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const settingsRoute: MlRoute = { +export const settingsRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index fdf29406893ad..6486db818e113 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -11,6 +11,8 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../contexts/kibana'; + import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { TimeSeriesExplorer } from '../../timeseriesexplorer'; @@ -34,15 +36,15 @@ import { MlRoute, PageLoader, PageProps } from '../router'; import { useRefresh } from '../use_refresh'; import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; -export const timeSeriesExplorerRoute: MlRoute = { +export const timeSeriesExplorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/timeseriesexplorer', render: (props, deps) => , breadcrumbs: [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), { text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { defaultMessage: 'Single Metric Viewer', @@ -50,7 +52,7 @@ export const timeSeriesExplorerRoute: MlRoute = { href: '', }, ], -}; +}); const PageWrapper: FC = ({ deps }) => { const { context, results } = useResolver('', undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts index b5c01a1c26144..428060dd2c31b 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts @@ -9,6 +9,7 @@ import { getUrlForRecord, isValidLabel, isValidTimeRange, + openCustomUrlWindow, } from './custom_url_utils'; import { AnomalyRecordDoc } from '../../../common/types/anomalies'; import { @@ -474,4 +475,49 @@ describe('ML - custom URL utils', () => { expect(isValidTimeRange('AUTO')).toBe(false); }); }); + + describe('openCustomUrlWindow', () => { + const originalOpen = window.open; + + beforeEach(() => { + delete (window as any).open; + const mockOpen = jest.fn(); + window.open = mockOpen; + }); + + afterEach(() => { + window.open = originalOpen; + }); + + it('should add the base path to a relative non-kibana url', () => { + openCustomUrlWindow( + 'the-url', + { url_name: 'the-url-name', url_value: 'the-url-value' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith('the-base-path/the-url', '_blank'); + }); + + it('should add the base path and `app` prefix to a relative kibana url', () => { + openCustomUrlWindow( + 'discover#/the-url', + { url_name: 'the-url-name', url_value: 'discover#/the-url-value' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith('the-base-path/app/discover#/the-url', '_blank'); + }); + + it('should use an absolute url with protocol as is', () => { + openCustomUrlWindow( + 'http://example.com', + { url_name: 'the-url-name', url_value: 'http://example.com' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith( + 'http://example.com', + '_blank', + 'noopener,noreferrer' + ); + }); + }); }); diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts index 20bb1c7f60597..9c843af36192e 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts @@ -76,15 +76,20 @@ export function getUrlForRecord( // Opens the specified URL in a new window. The behaviour (for example whether // it opens in a new tab or window) is determined from the original configuration // object which indicates whether it is opening a Kibana page running on the same server. -// fullUrl is the complete URL, including the base path, with any dollar delimited tokens -// from the urlConfig having been substituted with values from an anomaly record. -export function openCustomUrlWindow(fullUrl: string, urlConfig: UrlConfig) { +// `url` is the URL with any dollar delimited tokens from the urlConfig +// having been substituted with values from an anomaly record. +export function openCustomUrlWindow(url: string, urlConfig: UrlConfig, basePath: string) { // Run through a regex to test whether the url_value starts with a protocol scheme. if (/^(?:[a-z]+:)?\/\//i.test(urlConfig.url_value) === false) { - window.open(fullUrl, '_blank'); + // If `url` is a relative path, we need to prefix the base path. + if (url.charAt(0) !== '/') { + url = `${basePath}${isKibanaUrl(urlConfig) ? '/app/' : '/'}${url}`; + } + + window.open(url, '_blank'); } else { // Add noopener and noreferrr properties for external URLs. - const newWindow = window.open(fullUrl, '_blank', 'noopener,noreferrer'); + const newWindow = window.open(url, '_blank', 'noopener,noreferrer'); // Expect newWindow to be null, but just in case if not, reset the opener link. if (newWindow !== undefined && newWindow !== null) { @@ -94,13 +99,24 @@ export function openCustomUrlWindow(fullUrl: string, urlConfig: UrlConfig) { } // Returns whether the url_value of the supplied config is for -// a Kibana Discover or Dashboard page running on the same server as this ML plugin. +// a Kibana Discover, Dashboard or supported solution page running +// on the same server as this ML plugin. This is necessary so we can have +// backwards compatibility with custom URLs created before the move to +// BrowserRouter and URLs without hashes. If we add another solution to +// recognize modules or with custom UI in the custom URL builder we'd +// need to add the solution here. Manually created custom URLs for other +// solution pages need to be prefixed with `app/` in the custom URL builder. function isKibanaUrl(urlConfig: UrlConfig) { const urlValue = urlConfig.url_value; return ( + // HashRouter based plugins urlValue.startsWith('discover#/') || urlValue.startsWith('dashboards#/') || - urlValue.startsWith('apm#/') + urlValue.startsWith('apm#/') || + // BrowserRouter based plugins + urlValue.startsWith('security/') || + // Legacy links + urlValue.startsWith('siem#/') ); } diff --git a/x-pack/plugins/ml/public/url_generator.test.ts b/x-pack/plugins/ml/public/url_generator.test.ts index 45e2932b7781a..21dde12404957 100644 --- a/x-pack/plugins/ml/public/url_generator.test.ts +++ b/x-pack/plugins/ml/public/url_generator.test.ts @@ -19,7 +19,7 @@ describe('MlUrlGenerator', () => { mlExplorerSwimlane: { viewByFromPage: 2, viewByPerPage: 20 }, }); expect(url).toBe( - '/app/ml#/explorer?_g=(ml:(jobIds:!(test-job)))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20))' + '/app/ml/explorer#?_g=(ml:(jobIds:!(test-job)))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20))' ); }); diff --git a/x-pack/plugins/ml/public/url_generator.ts b/x-pack/plugins/ml/public/url_generator.ts index c2b57f6349d81..b7cf64159a827 100644 --- a/x-pack/plugins/ml/public/url_generator.ts +++ b/x-pack/plugins/ml/public/url_generator.ts @@ -83,7 +83,7 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition('_g', queryState, { useHash: false }, url); url = setStateToKbnUrl('_a', appState, { useHash: false }, url); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ff8c3186d5cf8..c81aade2b063e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11669,7 +11669,6 @@ "xpack.ml.jobMessages.timeLabel": "時間", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高度な構成", "xpack.ml.jobsBreadcrumbs.categorizationLabel": "カテゴリー分け", - "xpack.ml.jobsBreadcrumbs.jobWizardLabel": "ジョブを作成", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "マルチメトリック", "xpack.ml.jobsBreadcrumbs.populationLabel": "集団", "xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel": "ジョブを作成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bcf2f6707e7bf..aba5adf72c2f8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11672,7 +11672,6 @@ "xpack.ml.jobMessages.timeLabel": "时间", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高级配置", "xpack.ml.jobsBreadcrumbs.categorizationLabel": "归类", - "xpack.ml.jobsBreadcrumbs.jobWizardLabel": "创建作业", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "多指标", "xpack.ml.jobsBreadcrumbs.populationLabel": "填充", "xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel": "创建作业", From 10f8beae8ecb64407df758d44601802cb768e35a Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 31 Jul 2020 11:52:00 +0200 Subject: [PATCH 24/55] [Discover] Context unskip date nanos functional tests (#73781) * Use _source value of timestamp for search_after since ES allows this now * Unskip functional tests * Remove unused convertIsoToNanosAsStr --- .../angular/context/api/utils/date_conversion.ts | 9 --------- .../context/api/utils/get_es_query_search_after.ts | 10 ++-------- test/functional/apps/context/_date_nanos.js | 3 +-- .../apps/context/_date_nanos_custom_timestamp.js | 5 +---- 4 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts b/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts index 64544a335c911..4369234a3ce9a 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts @@ -31,15 +31,6 @@ export function extractNanos(timeFieldValue: string = ''): string { return fractionSeconds.length !== 9 ? fractionSeconds.padEnd(9, '0') : fractionSeconds; } -/** - * extract the nanoseconds as string of a given ISO formatted timestamp - */ -export function convertIsoToNanosAsStr(isoValue: string): string { - const nanos = extractNanos(isoValue); - const millis = convertIsoToMillis(isoValue); - return `${millis}${nanos.substr(3, 6)}`; -} - /** * convert an iso formatted string to number of milliseconds since * 1970-01-01T00:00:00.000Z diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index d4ee9e0e0f287..24ac19a7e3bc3 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { convertIsoToNanosAsStr } from './date_conversion'; import { SurrDocType, EsHitRecordList, EsHitRecord } from '../context'; export type EsQuerySearchAfter = [string | number, string | number]; @@ -38,15 +37,10 @@ export function getEsQuerySearchAfter( // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - const afterTimeValue = nanoSeconds - ? convertIsoToNanosAsStr(afterTimeDoc.fields[timeFieldName][0]) - : afterTimeDoc.sort[0]; + const afterTimeValue = nanoSeconds ? afterTimeDoc._source[timeFieldName] : afterTimeDoc.sort[0]; return [afterTimeValue, afterTimeDoc.sort[1]]; } // if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser // ES search_after also works when number is provided as string - return [ - nanoSeconds ? convertIsoToNanosAsStr(anchor.fields[timeFieldName][0]) : anchor.sort[0], - anchor.sort[1], - ]; + return [nanoSeconds ? anchor._source[timeFieldName] : anchor.sort[0], anchor.sort[1]]; } diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index 89769caaea253..cdf2d6c04be83 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -30,8 +30,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - // FLAKY/FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/58815 - describe.skip('context view for date_nanos', () => { + describe('context view for date_nanos', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await esArchiver.loadIfNeeded('date_nanos'); diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index 6329f6c431e6a..8fe08d13af0aa 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -30,10 +30,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - // skipped due to a recent change in ES that caused search_after queries with data containing - // custom timestamp formats like in the testdata to fail - // https://github.com/elastic/kibana/issues/58815 - describe.skip('context view for date_nanos with custom timestamp', () => { + describe('context view for date_nanos with custom timestamp', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_custom']); await esArchiver.loadIfNeeded('date_nanos_custom'); From dbb603f9793d6477fb73663d53216cbefa3b1d2e Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Fri, 31 Jul 2020 09:21:47 -0400 Subject: [PATCH 25/55] [Canvas][tech-debt] Ensure cursor is called until full results are received (#73347) * Ensure cursor is called until full results are receeived * Fix Typecheck * Convert dependencies to typescript * Fix typings Co-authored-by: Elastic Machine --- .../functions/server/esdocs.ts | 1 - .../functions/server/essql.ts | 1 - ...uild_bool_array.js => build_bool_array.ts} | 5 +- x-pack/plugins/canvas/server/lib/filters.js | 38 ------- x-pack/plugins/canvas/server/lib/filters.ts | 74 ++++++++++++ .../{get_es_filter.js => get_es_filter.ts} | 8 +- .../{normalize_type.js => normalize_type.ts} | 4 +- .../plugins/canvas/server/lib/query_es_sql.js | 59 ---------- .../canvas/server/lib/query_es_sql.test.ts | 106 ++++++++++++++++++ .../plugins/canvas/server/lib/query_es_sql.ts | 96 ++++++++++++++++ .../{sanitize_name.js => sanitize_name.ts} | 4 +- .../server/routes/es_fields/es_fields.ts | 1 - x-pack/plugins/canvas/types/filters.ts | 32 ++++++ x-pack/plugins/canvas/types/index.ts | 1 + 14 files changed, 320 insertions(+), 110 deletions(-) rename x-pack/plugins/canvas/server/lib/{build_bool_array.js => build_bool_array.ts} (66%) delete mode 100644 x-pack/plugins/canvas/server/lib/filters.js create mode 100644 x-pack/plugins/canvas/server/lib/filters.ts rename x-pack/plugins/canvas/server/lib/{get_es_filter.js => get_es_filter.ts} (75%) rename x-pack/plugins/canvas/server/lib/{normalize_type.js => normalize_type.ts} (89%) delete mode 100644 x-pack/plugins/canvas/server/lib/query_es_sql.js create mode 100644 x-pack/plugins/canvas/server/lib/query_es_sql.test.ts create mode 100644 x-pack/plugins/canvas/server/lib/query_es_sql.ts rename x-pack/plugins/canvas/server/lib/{sanitize_name.js => sanitize_name.ts} (85%) create mode 100644 x-pack/plugins/canvas/types/filters.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts index a090f09a76ea2..23fbc912d739a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts @@ -7,7 +7,6 @@ import squel from 'squel'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; /* eslint-disable */ -// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts index 5ac91bec849c2..2e053f9084296 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts @@ -6,7 +6,6 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; /* eslint-disable */ -// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/server/lib/build_bool_array.js b/x-pack/plugins/canvas/server/lib/build_bool_array.ts similarity index 66% rename from x-pack/plugins/canvas/server/lib/build_bool_array.js rename to x-pack/plugins/canvas/server/lib/build_bool_array.ts index f1cab93ceebbb..bd418394cf375 100644 --- a/x-pack/plugins/canvas/server/lib/build_bool_array.js +++ b/x-pack/plugins/canvas/server/lib/build_bool_array.ts @@ -5,10 +5,11 @@ */ import { getESFilter } from './get_es_filter'; +import { ExpressionValueFilter } from '../../types'; -const compact = (arr) => (Array.isArray(arr) ? arr.filter((val) => Boolean(val)) : []); +const compact = (arr: T[]) => (Array.isArray(arr) ? arr.filter((val) => Boolean(val)) : []); -export function buildBoolArray(canvasQueryFilterArray) { +export function buildBoolArray(canvasQueryFilterArray: ExpressionValueFilter[]) { return compact( canvasQueryFilterArray.map((clause) => { try { diff --git a/x-pack/plugins/canvas/server/lib/filters.js b/x-pack/plugins/canvas/server/lib/filters.js deleted file mode 100644 index afa58c7ee30c2..0000000000000 --- a/x-pack/plugins/canvas/server/lib/filters.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - TODO: This could be pluggable -*/ - -export function time(filter) { - if (!filter.column) { - throw new Error('column is required for Elasticsearch range filters'); - } - return { - range: { - [filter.column]: { gte: filter.from, lte: filter.to }, - }, - }; -} - -export function luceneQueryString(filter) { - return { - query_string: { - query: filter.query || '*', - }, - }; -} - -export function exactly(filter) { - return { - term: { - [filter.column]: { - value: filter.value, - }, - }, - }; -} diff --git a/x-pack/plugins/canvas/server/lib/filters.ts b/x-pack/plugins/canvas/server/lib/filters.ts new file mode 100644 index 0000000000000..9997640154e2c --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/filters.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + FilterType, + ExpressionValueFilter, + CanvasTimeFilter, + CanvasLuceneFilter, + CanvasExactlyFilter, +} from '../../types'; + +/* + TODO: This could be pluggable +*/ + +const isTimeFilter = ( + maybeTimeFilter: ExpressionValueFilter +): maybeTimeFilter is CanvasTimeFilter => { + return maybeTimeFilter.filterType === FilterType.time; +}; +const isLuceneFilter = ( + maybeLuceneFilter: ExpressionValueFilter +): maybeLuceneFilter is CanvasLuceneFilter => { + return maybeLuceneFilter.filterType === FilterType.luceneQueryString; +}; +const isExactlyFilter = ( + maybeExactlyFilter: ExpressionValueFilter +): maybeExactlyFilter is CanvasExactlyFilter => { + return maybeExactlyFilter.filterType === FilterType.exactly; +}; + +export function time(filter: ExpressionValueFilter) { + if (!isTimeFilter(filter) || !filter.column) { + throw new Error('column is required for Elasticsearch range filters'); + } + return { + range: { + [filter.column]: { gte: filter.from, lte: filter.to }, + }, + }; +} + +export function luceneQueryString(filter: ExpressionValueFilter) { + if (!isLuceneFilter(filter)) { + throw new Error('Filter is not a lucene filter'); + } + return { + query_string: { + query: filter.query || '*', + }, + }; +} + +export function exactly(filter: ExpressionValueFilter) { + if (!isExactlyFilter(filter)) { + throw new Error('Filter is not an exactly filter'); + } + return { + term: { + [filter.column]: { + value: filter.value, + }, + }, + }; +} + +export const filters: Record = { + exactly, + time, + luceneQueryString, +}; diff --git a/x-pack/plugins/canvas/server/lib/get_es_filter.js b/x-pack/plugins/canvas/server/lib/get_es_filter.ts similarity index 75% rename from x-pack/plugins/canvas/server/lib/get_es_filter.js rename to x-pack/plugins/canvas/server/lib/get_es_filter.ts index 7c025ed8dee9b..acc222ecc376f 100644 --- a/x-pack/plugins/canvas/server/lib/get_es_filter.js +++ b/x-pack/plugins/canvas/server/lib/get_es_filter.ts @@ -10,11 +10,11 @@ filter is the abstracted canvas filter. */ -/*eslint import/namespace: ['error', { allowComputed: true }]*/ -import * as filters from './filters'; +import { filters } from './filters'; +import { ExpressionValueFilter } from '../../types'; -export function getESFilter(filter) { - if (!filters[filter.filterType]) { +export function getESFilter(filter: ExpressionValueFilter) { + if (!filter.filterType || !filters[filter.filterType]) { throw new Error(`Unknown filter type: ${filter.filterType}`); } diff --git a/x-pack/plugins/canvas/server/lib/normalize_type.js b/x-pack/plugins/canvas/server/lib/normalize_type.ts similarity index 89% rename from x-pack/plugins/canvas/server/lib/normalize_type.js rename to x-pack/plugins/canvas/server/lib/normalize_type.ts index fda2fbe631646..b684325aacba9 100644 --- a/x-pack/plugins/canvas/server/lib/normalize_type.js +++ b/x-pack/plugins/canvas/server/lib/normalize_type.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export function normalizeType(type) { - const normalTypes = { +export function normalizeType(type: string) { + const normalTypes: Record = { string: ['string', 'text', 'keyword', '_type', '_id', '_index', 'geo_point'], number: [ 'float', diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.js b/x-pack/plugins/canvas/server/lib/query_es_sql.js deleted file mode 100644 index 442703b00ea3a..0000000000000 --- a/x-pack/plugins/canvas/server/lib/query_es_sql.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { map, zipObject } from 'lodash'; -import { buildBoolArray } from './build_bool_array'; -import { sanitizeName } from './sanitize_name'; -import { normalizeType } from './normalize_type'; - -export const queryEsSQL = (elasticsearchClient, { count, query, filter, timezone }) => - elasticsearchClient('transport.request', { - path: '/_sql?format=json', - method: 'POST', - body: { - query, - time_zone: timezone, - fetch_size: count, - client_id: 'canvas', - filter: { - bool: { - must: [{ match_all: {} }, ...buildBoolArray(filter)], - }, - }, - }, - }) - .then((res) => { - const columns = res.columns.map(({ name, type }) => { - return { name: sanitizeName(name), type: normalizeType(type) }; - }); - const columnNames = map(columns, 'name'); - const rows = res.rows.map((row) => zipObject(columnNames, row)); - - if (!!res.cursor) { - elasticsearchClient('transport.request', { - path: '/_sql/close', - method: 'POST', - body: { - cursor: res.cursor, - }, - }).catch((e) => { - throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); - }); - } - - return { - type: 'datatable', - columns, - rows, - }; - }) - .catch((e) => { - if (e.message.indexOf('parsing_exception') > -1) { - throw new Error( - `Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: ${e.message}` - ); - } - throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); - }); diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts new file mode 100644 index 0000000000000..c3c122d1e301a --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { zipObject } from 'lodash'; +import { queryEsSQL } from './query_es_sql'; +// @ts-expect-error +import { buildBoolArray } from './build_bool_array'; + +const response = { + columns: [ + { name: 'One', type: 'keyword' }, + { name: 'Two', type: 'keyword' }, + ], + rows: [ + ['foo', 'bar'], + ['buz', 'baz'], + ], + cursor: 'cursor-value', +}; + +const baseArgs = { + count: 1, + query: 'query', + filter: [], + timezone: 'timezone', +}; + +const getApi = (resp = response) => { + const api = jest.fn(); + api.mockResolvedValue(resp); + return api; +}; + +describe('query_es_sql', () => { + it('should call the api with the given args', async () => { + const api = getApi(); + + queryEsSQL(api, baseArgs); + + expect(api).toHaveBeenCalled(); + const givenArgs = api.mock.calls[0][1]; + + expect(givenArgs.body.fetch_size).toBe(baseArgs.count); + expect(givenArgs.body.query).toBe(baseArgs.query); + expect(givenArgs.body.time_zone).toBe(baseArgs.timezone); + }); + + it('formats the response', async () => { + const api = getApi(); + + const result = await queryEsSQL(api, baseArgs); + + const expectedColumns = response.columns.map((c) => ({ name: c.name, type: 'string' })); + const columnNames = expectedColumns.map((c) => c.name); + const expectedRows = response.rows.map((r) => zipObject(columnNames, r)); + + expect(result.type).toBe('datatable'); + expect(result.columns).toEqual(expectedColumns); + expect(result.rows).toEqual(expectedRows); + }); + + it('fetches pages until it has the requested count', async () => { + const pageOne = { + columns: [ + { name: 'One', type: 'keyword' }, + { name: 'Two', type: 'keyword' }, + ], + rows: [['foo', 'bar']], + cursor: 'cursor-value', + }; + + const pageTwo = { + rows: [['buz', 'baz']], + }; + + const api = getApi(pageOne); + api.mockReturnValueOnce(pageOne).mockReturnValueOnce(pageTwo); + + const result = await queryEsSQL(api, { ...baseArgs, count: 2 }); + expect(result.rows).toHaveLength(2); + }); + + it('closes any cursors that remain open', async () => { + const api = getApi(); + + await queryEsSQL(api, baseArgs); + expect(api.mock.calls[1][1].body.cursor).toBe(response.cursor); + }); + + it('throws on errors', async () => { + const api = getApi(); + api.mockRejectedValueOnce(new Error('parsing_exception')); + api.mockRejectedValueOnce(new Error('generic es error')); + + expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: parsing_exception"` + ); + + expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unexpected error from Elasticsearch: generic es error"` + ); + }); +}); diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.ts new file mode 100644 index 0000000000000..8639cfa31dca8 --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { map, zipObject } from 'lodash'; +import { buildBoolArray } from './build_bool_array'; +import { sanitizeName } from './sanitize_name'; +import { normalizeType } from './normalize_type'; +import { LegacyAPICaller } from '../../../../../src/core/server'; +import { ExpressionValueFilter } from '../../types'; + +interface Args { + count: number; + query: string; + timezone?: string; + filter: ExpressionValueFilter[]; +} + +interface CursorResponse { + cursor?: string; + rows: string[][]; +} + +type QueryResponse = CursorResponse & { + columns: Array<{ + name: string; + type: string; + }>; + cursor?: string; + rows: string[][]; +}; + +export const queryEsSQL = async ( + elasticsearchClient: LegacyAPICaller, + { count, query, filter, timezone }: Args +) => { + try { + let response: QueryResponse = await elasticsearchClient('transport.request', { + path: '/_sql?format=json', + method: 'POST', + body: { + query, + time_zone: timezone, + fetch_size: count, + client_id: 'canvas', + filter: { + bool: { + must: [{ match_all: {} }, ...buildBoolArray(filter)], + }, + }, + }, + }); + + const columns = response.columns.map(({ name, type }) => { + return { name: sanitizeName(name), type: normalizeType(type) }; + }); + const columnNames = map(columns, 'name'); + let rows = response.rows.map((row) => zipObject(columnNames, row)); + + while (rows.length < count && response.cursor !== undefined) { + response = await elasticsearchClient('transport.request', { + path: '/_sql?format=json', + method: 'POST', + body: { + cursor: response.cursor, + }, + }); + + rows = [...rows, ...response.rows.map((row) => zipObject(columnNames, row))]; + } + + if (response.cursor !== undefined) { + elasticsearchClient('transport.request', { + path: '/_sql/close', + method: 'POST', + body: { + cursor: response.cursor, + }, + }); + } + + return { + type: 'datatable', + columns, + rows, + }; + } catch (e) { + if (e.message.indexOf('parsing_exception') > -1) { + throw new Error( + `Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: ${e.message}` + ); + } + throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); + } +}; diff --git a/x-pack/plugins/canvas/server/lib/sanitize_name.js b/x-pack/plugins/canvas/server/lib/sanitize_name.ts similarity index 85% rename from x-pack/plugins/canvas/server/lib/sanitize_name.js rename to x-pack/plugins/canvas/server/lib/sanitize_name.ts index 4c787c816a331..781ab20509b36 100644 --- a/x-pack/plugins/canvas/server/lib/sanitize_name.js +++ b/x-pack/plugins/canvas/server/lib/sanitize_name.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export function sanitizeName(name) { +export function sanitizeName(name: string) { // invalid characters const invalid = ['(', ')']; const pattern = invalid.map((v) => escapeRegExp(v)).join('|'); @@ -12,6 +12,6 @@ export function sanitizeName(name) { return name.replace(regex, '_'); } -function escapeRegExp(string) { +function escapeRegExp(string: string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts index 7a9830124e305..000b7f6029952 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts @@ -8,7 +8,6 @@ import { mapValues, keys } from 'lodash'; import { schema } from '@kbn/config-schema'; import { API_ROUTE } from '../../../common/lib'; import { catchErrorHandler } from '../catch_error_handler'; -// @ts-expect-error unconverted lib import { normalizeType } from '../../lib/normalize_type'; import { RouteInitializerDeps } from '..'; diff --git a/x-pack/plugins/canvas/types/filters.ts b/x-pack/plugins/canvas/types/filters.ts new file mode 100644 index 0000000000000..356ebbbb76ac0 --- /dev/null +++ b/x-pack/plugins/canvas/types/filters.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 { ExpressionValueFilter } from '.'; + +export enum FilterType { + luceneQueryString = 'luceneQueryString', + time = 'time', + exactly = 'exactly', +} + +export type CanvasTimeFilter = ExpressionValueFilter & { + filterType: typeof FilterType.time; + to: string; + from: string; +}; + +export type CanvasLuceneFilter = ExpressionValueFilter & { + filterType: typeof FilterType.luceneQueryString; + query: string; +}; + +export type CanvasExactlyFilter = ExpressionValueFilter & { + filterType: typeof FilterType.exactly; + value: string; + column: string; +}; + +export type CanvasFilter = CanvasTimeFilter | CanvasExactlyFilter | CanvasLuceneFilter; diff --git a/x-pack/plugins/canvas/types/index.ts b/x-pack/plugins/canvas/types/index.ts index 0799627ce9b5a..f39c2d4367f9e 100644 --- a/x-pack/plugins/canvas/types/index.ts +++ b/x-pack/plugins/canvas/types/index.ts @@ -8,6 +8,7 @@ export * from '../../../../src/plugins/expressions/common'; export * from './assets'; export * from './canvas'; export * from './elements'; +export * from './filters'; export * from './functions'; export * from './renderers'; export * from './shortcuts'; From c66ea65ec1ca28172b93f4f5db8ae24e7023940e Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 31 Jul 2020 15:22:04 +0200 Subject: [PATCH 26/55] [APM] Use apmEventClient for querying APM event indices (#73449) Co-authored-by: Elastic Machine --- x-pack/plugins/apm/common/processor_event.ts | 14 + x-pack/plugins/apm/common/projections.ts | 16 ++ .../apm/common/projections/services.ts | 64 ----- .../app/ErrorGroupOverview/index.tsx | 4 +- .../components/app/RumDashboard/index.tsx | 4 +- .../components/app/ServiceMetrics/index.tsx | 4 +- .../app/ServiceNodeOverview/index.tsx | 4 +- .../components/app/ServiceOverview/index.tsx | 4 +- .../components/app/TraceOverview/index.tsx | 4 +- .../app/TransactionDetails/index.tsx | 4 +- .../app/TransactionOverview/index.tsx | 4 +- .../components/shared/KueryBar/index.tsx | 2 +- .../shared/LocalUIFilters/index.tsx | 4 +- .../shared/charts/MetricsChart/index.tsx | 2 +- .../context/UrlParamsContext/helpers.ts | 7 +- .../public/context/UrlParamsContext/types.ts | 4 +- .../public/hooks/useDynamicIndexPattern.ts | 4 +- .../apm/public/hooks/useLocalUIFilters.ts | 4 +- .../plugins/apm/public/utils/testHelpers.tsx | 5 +- .../get_all_environments.test.ts.snap | 42 +-- .../lib/environments/get_all_environments.ts | 25 +- .../errors/__snapshots__/queries.test.ts.snap | 33 ++- .../__snapshots__/queries.test.ts.snap | 22 +- .../__snapshots__/get_buckets.test.ts.snap | 11 +- .../__tests__/get_buckets.test.ts | 8 +- .../lib/errors/distribution/get_buckets.ts | 11 +- .../apm/server/lib/errors/get_error_group.ts | 12 +- .../apm/server/lib/errors/get_error_groups.ts | 25 +- .../call_client_with_debug.ts | 72 +++++ .../add_filter_to_exclude_legacy_data.ts | 31 +++ .../create_apm_event_client/index.ts | 91 +++++++ .../unpack_processor_events.ts | 61 +++++ .../create_internal_es_client/index.ts | 66 +++++ .../apm/server/lib/helpers/es_client.test.ts | 48 ---- .../apm/server/lib/helpers/es_client.ts | 228 ---------------- .../server/lib/helpers/setup_request.test.ts | 250 +++++++++--------- .../apm/server/lib/helpers/setup_request.ts | 33 +-- .../get_dynamic_index_pattern.ts | 19 +- .../__snapshots__/queries.test.ts.snap | 165 ++++++------ .../java/gc/fetch_and_transform_gc_metrics.ts | 8 +- .../metrics/fetch_and_transform_metrics.ts | 12 +- .../get_service_count.ts | 34 +-- .../get_transaction_coordinates.ts | 14 +- .../lib/observability_overview/has_data.ts | 32 +-- .../__snapshots__/queries.test.ts.snap | 44 ++- .../lib/rum_client/get_client_metrics.ts | 8 +- .../rum_client/get_page_load_distribution.ts | 12 +- .../lib/rum_client/get_page_view_trends.ts | 8 +- .../lib/rum_client/get_pl_dist_breakdown.ts | 18 +- .../server/lib/rum_client/get_rum_services.ts | 8 +- .../lib/rum_client/get_visitor_breakdown.ts | 8 +- .../fetch_service_paths_from_trace_ids.ts | 22 +- .../server/lib/service_map/get_service_map.ts | 8 +- .../get_service_map_service_node_info.test.ts | 4 +- .../get_service_map_service_node_info.ts | 42 +-- .../lib/service_map/get_trace_sample_ids.ts | 17 +- .../__snapshots__/queries.test.ts.snap | 33 ++- .../apm/server/lib/service_nodes/index.ts | 8 +- .../__snapshots__/queries.test.ts.snap | 153 ++++------- .../get_derived_service_annotations.ts | 25 +- .../lib/services/get_service_agent_name.ts | 21 +- .../lib/services/get_service_node_metadata.ts | 8 +- .../services/get_service_transaction_types.ts | 11 +- .../get_services/get_legacy_data_status.ts | 19 +- .../get_services/get_services_items.ts | 4 +- .../get_services/get_services_items_stats.ts | 132 +++------ .../get_services/has_historical_agent_data.ts | 39 +-- .../__snapshots__/queries.test.ts.snap | 27 +- .../create_or_update_configuration.ts | 2 +- .../get_agent_name_by_service.ts | 29 +- .../agent_configuration/get_service_names.ts | 31 +-- .../get_transaction.test.ts.snap | 25 +- .../create_or_update_custom_link.ts | 2 +- .../settings/custom_link/get_transaction.ts | 15 +- .../traces/__snapshots__/queries.test.ts.snap | 11 +- .../apm/server/lib/traces/get_trace_items.ts | 32 +-- .../__snapshots__/queries.test.ts.snap | 77 +++--- .../server/lib/transaction_groups/fetcher.ts | 9 +- .../lib/transaction_groups/get_error_rate.ts | 10 +- .../get_transaction_group_stats.ts | 11 +- .../__snapshots__/queries.test.ts.snap | 77 +++--- .../avg_duration_by_browser/fetcher.test.ts | 2 +- .../avg_duration_by_browser/fetcher.ts | 10 +- .../avg_duration_by_country/index.ts | 11 +- .../lib/transactions/breakdown/index.test.ts | 2 +- .../lib/transactions/breakdown/index.ts | 11 +- .../__snapshots__/fetcher.test.ts.snap | 11 +- .../get_timeseries_data/fetcher.test.ts | 14 +- .../charts/get_timeseries_data/fetcher.ts | 11 +- .../distribution/get_buckets/fetcher.ts | 12 +- .../distribution/get_distribution_max.ts | 11 +- .../lib/transactions/get_transaction/index.ts | 14 +- .../get_transaction_by_trace/index.ts | 16 +- .../__snapshots__/queries.test.ts.snap | 42 +-- .../server/lib/ui_filters/get_environments.ts | 23 +- .../__snapshots__/queries.test.ts.snap | 21 +- .../get_local_filter_query.ts | 4 +- .../lib/ui_filters/local_ui_filters/index.ts | 6 +- .../local_ui_filters/queries.test.ts | 2 +- .../{common => server}/projections/errors.ts | 15 +- .../{common => server}/projections/metrics.ts | 17 +- .../projections/rum_overview.ts | 13 +- .../projections/service_nodes.ts | 3 +- .../apm/server/projections/services.ts | 47 ++++ .../projections/transaction_groups.ts | 7 +- .../projections/transactions.ts | 15 +- .../{common => server}/projections/typings.ts | 16 +- .../util/merge_projection/index.test.ts | 21 +- .../util/merge_projection/index.ts | 8 +- .../plugins/apm/server/routes/ui_filters.ts | 16 +- 110 files changed, 1346 insertions(+), 1576 deletions(-) create mode 100644 x-pack/plugins/apm/common/projections.ts delete mode 100644 x-pack/plugins/apm/common/projections/services.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts delete mode 100644 x-pack/plugins/apm/server/lib/helpers/es_client.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/helpers/es_client.ts rename x-pack/plugins/apm/{common => server}/projections/errors.ts (70%) rename x-pack/plugins/apm/{common => server}/projections/metrics.ts (72%) rename x-pack/plugins/apm/{common => server}/projections/rum_overview.ts (71%) rename x-pack/plugins/apm/{common => server}/projections/service_nodes.ts (88%) create mode 100644 x-pack/plugins/apm/server/projections/services.ts rename x-pack/plugins/apm/{common => server}/projections/transaction_groups.ts (86%) rename x-pack/plugins/apm/{common => server}/projections/transactions.ts (76%) rename x-pack/plugins/apm/{common => server}/projections/typings.ts (56%) rename x-pack/plugins/apm/{common => server}/projections/util/merge_projection/index.test.ts (73%) rename x-pack/plugins/apm/{common => server}/projections/util/merge_projection/index.ts (82%) diff --git a/x-pack/plugins/apm/common/processor_event.ts b/x-pack/plugins/apm/common/processor_event.ts index 3e8b0ba0e8b5e..cd8bcaa1de237 100644 --- a/x-pack/plugins/apm/common/processor_event.ts +++ b/x-pack/plugins/apm/common/processor_event.ts @@ -8,4 +8,18 @@ export enum ProcessorEvent { transaction = 'transaction', error = 'error', metric = 'metric', + span = 'span', + onboarding = 'onboarding', + sourcemap = 'sourcemap', } +/** + * Processor events that are searchable in the UI via the query bar. + * + * Some client-sideroutes will define 1 or more processor events that + * will be used to fetch the dynamic index pattern for the query bar. + */ + +export type UIProcessorEvent = + | ProcessorEvent.transaction + | ProcessorEvent.error + | ProcessorEvent.metric; diff --git a/x-pack/plugins/apm/common/projections.ts b/x-pack/plugins/apm/common/projections.ts new file mode 100644 index 0000000000000..a5fd9d3951cc9 --- /dev/null +++ b/x-pack/plugins/apm/common/projections.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum Projection { + services = 'services', + transactionGroups = 'transactionGroups', + traces = 'traces', + transactions = 'transactions', + metrics = 'metrics', + errorGroups = 'errorGroups', + serviceNodes = 'serviceNodes', + rumOverview = 'rumOverview', +} diff --git a/x-pack/plugins/apm/common/projections/services.ts b/x-pack/plugins/apm/common/projections/services.ts deleted file mode 100644 index 809caeeaf6088..0000000000000 --- a/x-pack/plugins/apm/common/projections/services.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - Setup, - SetupUIFilters, - SetupTimeRange, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../server/lib/helpers/setup_request'; -import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; - -export function getServicesProjection({ - setup, - noEvents, -}: { - setup: Setup & SetupTimeRange & SetupUIFilters; - noEvents?: boolean; -}) { - const { start, end, uiFiltersES, indices } = setup; - - return { - ...(noEvents - ? {} - : { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], - }), - body: { - size: 0, - query: { - bool: { - filter: [ - ...(noEvents - ? [] - : [ - { - terms: { - [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'], - }, - }, - ]), - { range: rangeFilter(start, end) }, - ...uiFiltersES, - ], - }, - }, - aggs: { - services: { - terms: { - field: SERVICE_NAME, - }, - }, - }, - }, - }; -} diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index fe2303d645ec9..92ea044720531 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -14,7 +14,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { useFetcher } from '../../../hooks/useFetcher'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { callApmApi } from '../../../services/rest/createCallApmApi'; @@ -79,7 +79,7 @@ function ErrorGroupOverview() { params: { serviceName, }, - projection: PROJECTION.ERROR_GROUPS, + projection: Projection.errorGroups, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 9b88202b2e5ef..8d1959ec14d15 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -13,7 +13,7 @@ import { } from '@elastic/eui'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { RumDashboard } from './RumDashboard'; import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -28,7 +28,7 @@ export function RumOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionUrl', 'location', 'device', 'os', 'browser'], - projection: PROJECTION.RUM_OVERVIEW, + projection: Projection.rumOverview, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx index 9af6a8d988c11..9b01f9ebb7e99 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -16,7 +16,7 @@ import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; import { MetricsChart } from '../../shared/charts/MetricsChart'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; interface ServiceMetricsProps { @@ -36,7 +36,7 @@ export function ServiceMetrics({ agentName }: ServiceMetricsProps) { serviceName, serviceNodeName, }, - projection: PROJECTION.METRICS, + projection: Projection.metrics, showCount: false, }), [serviceName, serviceNodeName] diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 5537a73d228e8..3cde48aa483cb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ManagedTable, ITableColumn } from '../../shared/ManagedTable'; @@ -46,7 +46,7 @@ function ServiceNodeOverview() { params: { serviceName, }, - projection: PROJECTION.SERVICE_NODES, + projection: Projection.serviceNodes, }), [serviceName] ); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx index 7d05ae90afb87..7146e471a7f82 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -15,7 +15,7 @@ import { NoServicesMessage } from './NoServicesMessage'; import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; @@ -88,7 +88,7 @@ export function ServiceOverview() { const localFiltersConfig: React.ComponentProps = useMemo( () => ({ filterNames: ['host', 'agentName'], - projection: PROJECTION.SERVICES, + projection: Projection.services, }), [] ); diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index cdebb3aac129b..06b4459fb56eb 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -11,7 +11,7 @@ import { TraceList } from './TraceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; type TracesAPIResponse = APIReturnType<'/api/apm/traces'>; @@ -48,7 +48,7 @@ export function TraceOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionResult', 'host', 'containerId', 'podName'], - projection: PROJECTION.TRACES, + projection: Projection.traces, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index c4d5be5874215..0dc2f607b1ef2 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -27,7 +27,7 @@ import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { HeightRetainer } from '../../shared/HeightRetainer'; import { ErroneousTransactionsRateChart } from '../../shared/charts/ErroneousTransactionsRateChart'; @@ -52,7 +52,7 @@ export function TransactionDetails() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionResult', 'serviceVersion'], - projection: PROJECTION.TRANSACTIONS, + projection: Projection.transactions, params: { transactionName, transactionType, diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 98702fe3686ff..d9bd3e59d281f 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -35,7 +35,7 @@ import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../observability/public'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; @@ -103,7 +103,7 @@ export function TransactionOverview() { serviceName, transactionType, }, - projection: PROJECTION.TRANSACTION_GROUPS, + projection: Projection.transactionGroups, }), [serviceName, transactionType] ); diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 502f5f0034b5f..6c605886e6e00 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -9,7 +9,7 @@ import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; -// @ts-ignore +// @ts-expect-error import { Typeahead } from './Typeahead'; import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx index fedf96b4cc4ea..ba700e68b59bc 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -17,10 +17,10 @@ import styled from 'styled-components'; import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { Filter } from './Filter'; import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; interface Props { - projection: PROJECTION; + projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; showCount?: boolean; diff --git a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx index 61632700b81d8..5b167e8160ffa 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -7,7 +7,7 @@ import { EuiTitle } from '@elastic/eui'; import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; -// @ts-ignore +// @ts-expect-error import CustomPlot from '../CustomPlot'; import { asDecimal, diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts index d9781400f2272..65514ff71d02b 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts @@ -7,10 +7,13 @@ import { compact, pickBy } from 'lodash'; import datemath from '@elastic/datemath'; import { IUrlParams } from './types'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { + ProcessorEvent, + UIProcessorEvent, +} from '../../../common/processor_event'; interface PathParams { - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; serviceName?: string; errorGroupId?: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts index 78fe662b88d75..7b50a705afa33 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts @@ -6,7 +6,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { UIProcessorEvent } from '../../../common/processor_event'; export type IUrlParams = { detailTab?: string; @@ -32,6 +32,6 @@ export type IUrlParams = { pageSize?: number; serviceNodeName?: string; searchTerm?: string; - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; traceIdLink?: string; } & Partial>; diff --git a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts index 64f333d72f0f5..0b4978acdfcb1 100644 --- a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts +++ b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts @@ -5,10 +5,10 @@ */ import { useFetcher } from './useFetcher'; -import { ProcessorEvent } from '../../common/processor_event'; +import { UIProcessorEvent } from '../../common/processor_event'; export function useDynamicIndexPattern( - processorEvent: ProcessorEvent | undefined + processorEvent: UIProcessorEvent | undefined ) { const { data, status } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index 3354e676cf323..45ede7e7f2607 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -17,7 +17,7 @@ import { import { history } from '../utils/history'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; -import { PROJECTION } from '../../common/projections/typings'; +import { Projection } from '../../common/projections'; import { pickKeys } from '../../common/utils/pick_keys'; import { useCallApi } from './useCallApi'; @@ -35,7 +35,7 @@ export function useLocalUIFilters({ filterNames, params, }: { - projection: PROJECTION; + projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; }) { diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 418312743c324..e750102de2baa 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -99,10 +99,9 @@ export function expectTextsInDocument(output: any, texts: string[]) { } interface MockSetup { - dynamicIndexPattern: any; start: number; end: number; - client: any; + apmEventClient: any; internalClient: any; config: APMConfig; uiFiltersES: ESFilter[]; @@ -148,7 +147,7 @@ export async function inspectSearchParams( const mockSetup = { start: 1528113600000, end: 1528977600000, - client: { search: spy } as any, + apmEventClient: { search: spy } as any, internalClient: { search: spy } as any, config: new Proxy({}, { get: () => 'myIndex' }) as APMConfig, uiFiltersES: [{ term: { 'my.custom.ui.filter': 'foo-bar' } }], diff --git a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap index b943102b39de8..da2309afa07cf 100644 --- a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap @@ -2,6 +2,13 @@ exports[`getAllEnvironments fetches all environments 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -15,15 +22,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "term": Object { "service.name": "test", @@ -34,16 +32,18 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; exports[`getAllEnvironments fetches all environments with includeMissing 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -57,15 +57,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "term": Object { "service.name": "test", @@ -76,10 +67,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 9b17033a1f2a5..423b87cb78c3c 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; @@ -21,7 +21,7 @@ export async function getAllEnvironments({ setup: Setup; includeMissing?: boolean; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; // omit filter for service.name if "All" option is selected const serviceNameFilter = serviceName @@ -29,21 +29,18 @@ export async function getAllEnvironments({ : []; const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - ...serviceNameFilter, - ], + filter: [...serviceNameFilter], }, }, aggs: { @@ -58,7 +55,7 @@ export async function getAllEnvironments({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const environments = resp.aggregations?.environments.buckets.map( (bucket) => bucket.key as string diff --git a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap index 982ad558dc91d..63b6c9cde4d0d 100644 --- a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`error queries fetches a single error group 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "query": Object { "bool": Object { @@ -11,11 +16,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "error.grouping_key": "groupId", @@ -57,12 +57,16 @@ Object { }, ], }, - "index": "myIndex", } `; exports[`error queries fetches multiple error groups 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "error_groups": Object { @@ -104,11 +108,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -128,12 +127,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`error queries fetches multiple error groups when sortField = latestOccurrenceAt 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "error_groups": Object { @@ -180,11 +183,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -204,6 +202,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap index b71b2d697126a..ea142ca2acc00 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`error distribution queries fetches an error distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -19,11 +24,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "serviceName", @@ -48,12 +48,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`error distribution queries fetches an error distribution with a group id 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -71,11 +75,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "serviceName", @@ -105,6 +104,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap index d336d71424750..085bedf774c46 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap @@ -4,6 +4,11 @@ exports[`timeseriesFetcher should make the correct query 1`] = ` Array [ Array [ Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -21,11 +26,6 @@ Array [ "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "myServiceName", @@ -50,7 +50,6 @@ Array [ }, "size": 0, }, - "index": "apm-*", }, ], ] diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts index 5f23a9329a583..e0df4d7744610 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { getBuckets } from '../get_buckets'; import { APMConfig } from '../../../..'; +import { ProcessorEvent } from '../../../../../common/processor_event'; describe('timeseriesFetcher', () => { let clientSpy: jest.Mock; @@ -29,7 +29,7 @@ describe('timeseriesFetcher', () => { setup: { start: 1528113600000, end: 1528977600000, - client: { + apmEventClient: { search: clientSpy, } as any, internalClient: { @@ -66,8 +66,6 @@ describe('timeseriesFetcher', () => { it('should limit query results to error documents', () => { const query = clientSpy.mock.calls[0][0]; - expect(query.body.query.bool.filter).toEqual( - expect.arrayContaining([{ term: { [PROCESSOR_EVENT]: 'error' } }]) - ); + expect(query.apm.events).toEqual([ProcessorEvent.error]); }); }); diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index db36ad1ede91c..de6df15354e79 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { ESFilter } from '../../../../typings/elasticsearch'; import { ERROR_GROUP_ID, - PROCESSOR_EVENT, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../../../common/utils/range_filter'; @@ -28,9 +28,8 @@ export async function getBuckets({ bucketSize: number; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'error' } }, { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -41,7 +40,9 @@ export async function getBuckets({ } const params = { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error], + }, body: { size: 0, query: { @@ -65,7 +66,7 @@ export async function getBuckets({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const buckets = (resp.aggregations?.distribution.buckets || []).map( (bucket) => ({ diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts index 3d20f84ccfbc2..b23c955b57183 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { ERROR_GROUP_ID, - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_SAMPLED, } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; -import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, @@ -32,17 +31,18 @@ export async function getErrorGroup({ groupId: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error as const], + }, body: { size: 1, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { term: { [ERROR_GROUP_ID]: groupId } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -57,7 +57,7 @@ export async function getErrorGroup({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const error = resp.hits.hits[0]?._source; const transactionId = error?.transaction?.id; const traceId = error?.trace?.id; diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index ad216de271f37..ab1c2149be343 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -13,14 +13,13 @@ import { ERROR_LOG_MESSAGE, } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; -import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { getErrorGroupsProjection } from '../../../common/projections/errors'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getErrorGroupsProjection } from '../../projections/errors'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { SortOptions } from '../../../typings/elasticsearch/aggregations'; export type ErrorGroupListAPIResponse = PromiseReturnType< @@ -38,7 +37,7 @@ export async function getErrorGroups({ sortDirection?: 'asc' | 'desc'; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { client } = setup; + const { apmEventClient } = setup; // sort buckets by last occurrence of error const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; @@ -92,23 +91,7 @@ export async function getErrorGroups({ }, }); - interface SampleError { - '@timestamp': APMError['@timestamp']; - error: { - log?: { - message: string; - }; - exception?: Array<{ - handled?: boolean; - message?: string; - type?: string; - }>; - culprit: APMError['error']['culprit']; - grouping_key: APMError['error']['grouping_key']; - }; - } - - const resp = await client.search(params); + const resp = await apmEventClient.search(params); // aggregations can be undefined when no matching indices are found. // this is an exception rather than the rule so the ES type does not account for this. diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts new file mode 100644 index 0000000000000..c475640595227 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable no-console */ + +import chalk from 'chalk'; +import { + LegacyAPICaller, + KibanaRequest, +} from '../../../../../../../src/core/server'; + +function formatObj(obj: Record) { + return JSON.stringify(obj, null, 2); +} + +export async function callClientWithDebug({ + apiCaller, + operationName, + params, + debug, + request, +}: { + apiCaller: LegacyAPICaller; + operationName: string; + params: Record; + debug: boolean; + request: KibanaRequest; +}) { + const startTime = process.hrtime(); + + let res: any; + let esError = null; + try { + res = apiCaller(operationName, params); + } catch (e) { + // catch error and throw after outputting debug info + esError = e; + } + + if (debug) { + const highlightColor = esError ? 'bgRed' : 'inverse'; + const diff = process.hrtime(startTime); + const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; + const routeInfo = `${request.route.method.toUpperCase()} ${ + request.route.path + }`; + + console.log( + chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) + ); + + if (operationName === 'search') { + console.log(`GET ${params.index}/_${operationName}`); + console.log(formatObj(params.body)); + } else { + console.log(chalk.bold('ES operation:'), operationName); + + console.log(chalk.bold('ES query:')); + console.log(formatObj(params)); + } + console.log(`\n`); + } + + if (esError) { + throw esError; + } + + return res; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts new file mode 100644 index 0000000000000..494cd6cbf0eec --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep } from 'lodash'; +import { OBSERVER_VERSION_MAJOR } from '../../../../../common/elasticsearch_fieldnames'; +import { + ESSearchRequest, + ESFilter, +} from '../../../../../typings/elasticsearch'; + +/* + Adds a range query to the ES request to exclude legacy data +*/ + +export function addFilterToExcludeLegacyData( + params: ESSearchRequest & { + body: { query: { bool: { filter: ESFilter[] } } }; + } +) { + const nextParams = cloneDeep(params); + + // add filter for omitting pre-7.x data + nextParams.body.query.bool.filter.push({ + range: { [OBSERVER_VERSION_MAJOR]: { gte: 7 } }, + }); + + return nextParams; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts new file mode 100644 index 0000000000000..2bfd3c94ed34c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ValuesType } from 'utility-types'; +import { APMBaseDoc } from '../../../../../typings/es_schemas/raw/apm_base_doc'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { KibanaRequest } from '../../../../../../../../src/core/server'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { + ESSearchRequest, + ESSearchResponse, +} from '../../../../../typings/elasticsearch'; +import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices'; +import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { addFilterToExcludeLegacyData } from './add_filter_to_exclude_legacy_data'; +import { callClientWithDebug } from '../call_client_with_debug'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; +import { unpackProcessorEvents } from './unpack_processor_events'; + +export type APMEventESSearchRequest = Omit & { + apm: { + events: ProcessorEvent[]; + }; +}; + +type TypeOfProcessorEvent = { + [ProcessorEvent.error]: APMError; + [ProcessorEvent.transaction]: Transaction; + [ProcessorEvent.span]: Span; + [ProcessorEvent.metric]: APMBaseDoc; + [ProcessorEvent.onboarding]: unknown; + [ProcessorEvent.sourcemap]: unknown; +}[T]; + +type ESSearchRequestOf = Omit< + TParams, + 'apm' +> & { index: string[] | string }; + +type TypedSearchResponse< + TParams extends APMEventESSearchRequest +> = ESSearchResponse< + TypeOfProcessorEvent>, + ESSearchRequestOf +>; + +export type APMEventClient = ReturnType; + +export function createApmEventClient({ + context, + request, + indices, + options: { includeFrozen } = { includeFrozen: false }, +}: { + context: APMRequestHandlerContext; + request: KibanaRequest; + indices: ApmIndicesConfig; + options: { + includeFrozen: boolean; + }; +}) { + const client = context.core.elasticsearch.legacy.client; + + return { + search( + params: TParams, + { includeLegacyData } = { includeLegacyData: false } + ): Promise> { + const withProcessorEventFilter = unpackProcessorEvents(params, indices); + + const withPossibleLegacyDataFilter = !includeLegacyData + ? addFilterToExcludeLegacyData(withProcessorEventFilter) + : withProcessorEventFilter; + + return callClientWithDebug({ + apiCaller: client.callAsCurrentUser, + operationName: 'search', + params: { + ...withPossibleLegacyDataFilter, + ignore_throttled: !includeFrozen, + }, + request, + debug: context.params.query._debug, + }); + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts new file mode 100644 index 0000000000000..d35403ad35d94 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -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 { uniq, defaultsDeep, cloneDeep } from 'lodash'; +import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { + ESSearchRequest, + ESFilter, +} from '../../../../../typings/elasticsearch'; +import { APMEventESSearchRequest } from '.'; +import { + ApmIndicesConfig, + ApmIndicesName, +} from '../../../settings/apm_indices/get_apm_indices'; + +export const processorEventIndexMap: Record = { + [ProcessorEvent.transaction]: 'apm_oss.transactionIndices', + [ProcessorEvent.span]: 'apm_oss.spanIndices', + [ProcessorEvent.metric]: 'apm_oss.metricsIndices', + [ProcessorEvent.error]: 'apm_oss.errorIndices', + [ProcessorEvent.sourcemap]: 'apm_oss.sourcemapIndices', + [ProcessorEvent.onboarding]: 'apm_oss.onboardingIndices', +}; + +export function unpackProcessorEvents( + request: APMEventESSearchRequest, + indices: ApmIndicesConfig +) { + const { apm, ...params } = request; + + const index = uniq( + apm.events.map((event) => indices[processorEventIndexMap[event]]) + ); + + const withFilterForProcessorEvent: ESSearchRequest & { + body: { query: { bool: { filter: ESFilter[] } } }; + } = defaultsDeep(cloneDeep(params), { + body: { + query: { + bool: { + filter: [], + }, + }, + }, + }); + + withFilterForProcessorEvent.body.query.bool.filter.push({ + terms: { + [PROCESSOR_EVENT]: apm.events, + }, + }); + + return { + index, + ...withFilterForProcessorEvent, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts new file mode 100644 index 0000000000000..072391606d574 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IndexDocumentParams, + IndicesCreateParams, + DeleteDocumentResponse, + DeleteDocumentParams, +} from 'elasticsearch'; +import { KibanaRequest } from 'src/core/server'; +import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { + ESSearchResponse, + ESSearchRequest, +} from '../../../../../typings/elasticsearch'; +import { callClientWithDebug } from '../call_client_with_debug'; + +// `type` was deprecated in 7.0 +export type APMIndexDocumentParams = Omit, 'type'>; + +export type APMInternalClient = ReturnType; + +export function createInternalESClient({ + context, + request, +}: { + context: APMRequestHandlerContext; + request: KibanaRequest; +}) { + const { callAsInternalUser } = context.core.elasticsearch.legacy.client; + + const callEs = (operationName: string, params: Record) => { + return callClientWithDebug({ + apiCaller: callAsInternalUser, + operationName, + params, + request, + debug: context.params.query._debug, + }); + }; + + return { + search: async < + TDocument = unknown, + TSearchRequest extends ESSearchRequest = ESSearchRequest + >( + params: TSearchRequest + ): Promise> => { + return callEs('search', params); + }, + index: (params: APMIndexDocumentParams) => { + return callEs('index', params); + }, + delete: ( + params: Omit + ): Promise => { + return callEs('delete', params); + }, + indicesCreate: (params: IndicesCreateParams) => { + return callEs('indices.create', params); + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts deleted file mode 100644 index 61c9d751bf533..0000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isApmIndex } from './es_client'; - -describe('isApmIndex', () => { - const apmIndices = [ - 'apm-*-metric-*', - 'apm-*-onboarding-*', - 'apm-*-span-*', - 'apm-*-transaction-*', - 'apm-*-error-*', - ]; - describe('when indexParam is a string', () => { - it('should return true if it matches any of the items in apmIndices', () => { - const indexParam = 'apm-*-transaction-*'; - expect(isApmIndex(apmIndices, indexParam)).toBe(true); - }); - - it('should return false if it does not match any of the items in `apmIndices`', () => { - const indexParam = '.ml-anomalies-*'; - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - - describe('when indexParam is an array', () => { - it('should return true if all values in `indexParam` matches values in `apmIndices`', () => { - const indexParam = ['apm-*-transaction-*', 'apm-*-span-*']; - expect(isApmIndex(apmIndices, indexParam)).toBe(true); - }); - - it("should return false if some of the values don't match with `apmIndices`", () => { - const indexParam = ['apm-*-transaction-*', '.ml-anomalies-*']; - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - - describe('when indexParam is neither a string or an array', () => { - it('should return false', () => { - [true, false, undefined].forEach((indexParam) => { - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts deleted file mode 100644 index 2d730933e2473..0000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 no-console */ -import { - IndexDocumentParams, - SearchParams, - IndicesCreateParams, - DeleteDocumentResponse, - DeleteDocumentParams, -} from 'elasticsearch'; -import { cloneDeep, isString, merge } from 'lodash'; -import { KibanaRequest } from 'src/core/server'; -import chalk from 'chalk'; -import { - ESSearchRequest, - ESSearchResponse, -} from '../../../typings/elasticsearch'; -import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; -import { pickKeys } from '../../../common/utils/pick_keys'; -import { APMRequestHandlerContext } from '../../routes/typings'; -import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - -// `type` was deprecated in 7.0 -export type APMIndexDocumentParams = Omit, 'type'>; - -export interface IndexPrivileges { - has_all_requested: boolean; - index: Record; -} - -interface IndexPrivilegesParams { - index: Array<{ - names: string[] | string; - privileges: string[]; - }>; -} - -export function isApmIndex( - apmIndices: string[], - indexParam: SearchParams['index'] -) { - if (isString(indexParam)) { - return apmIndices.includes(indexParam); - } else if (Array.isArray(indexParam)) { - // return false if at least one of the indices is not an APM index - return indexParam.every((index) => apmIndices.includes(index)); - } - return false; -} - -function addFilterForLegacyData( - apmIndices: string[], - params: ESSearchRequest, - { includeLegacyData = false } = {} -): SearchParams { - // search across all data (including data) - if (includeLegacyData || !isApmIndex(apmIndices, params.index)) { - return params; - } - - const nextParams = merge( - { - body: { - query: { - bool: { - filter: [], - }, - }, - }, - }, - cloneDeep(params) - ); - - // add filter for omitting pre-7.x data - nextParams.body.query.bool.filter.push({ - range: { [OBSERVER_VERSION_MAJOR]: { gte: 7 } }, - }); - - return nextParams; -} - -// add additional params for search (aka: read) requests -function getParamsForSearchRequest({ - context, - params, - indices, - includeFrozen, - includeLegacyData, -}: { - context: APMRequestHandlerContext; - params: ESSearchRequest; - indices: ApmIndicesConfig; - includeFrozen: boolean; - includeLegacyData?: boolean; -}) { - // Get indices for legacy data filter (only those which apply) - const apmIndices = Object.values( - pickKeys( - indices, - 'apm_oss.sourcemapIndices', - 'apm_oss.errorIndices', - 'apm_oss.onboardingIndices', - 'apm_oss.spanIndices', - 'apm_oss.transactionIndices', - 'apm_oss.metricsIndices' - ) - ); - return { - ...addFilterForLegacyData(apmIndices, params, { includeLegacyData }), // filter out pre-7.0 data - ignore_throttled: !includeFrozen, // whether to query frozen indices or not - }; -} - -interface APMOptions { - includeLegacyData: boolean; -} - -interface ClientCreateOptions { - clientAsInternalUser?: boolean; - indices: ApmIndicesConfig; - includeFrozen: boolean; -} - -export type ESClient = ReturnType; - -function formatObj(obj: Record) { - return JSON.stringify(obj, null, 2); -} - -export function getESClient( - context: APMRequestHandlerContext, - request: KibanaRequest, - { clientAsInternalUser = false, indices, includeFrozen }: ClientCreateOptions -) { - const { - callAsCurrentUser, - callAsInternalUser, - } = context.core.elasticsearch.legacy.client; - - async function callEs(operationName: string, params: Record) { - const startTime = process.hrtime(); - - let res: any; - let esError = null; - try { - res = clientAsInternalUser - ? await callAsInternalUser(operationName, params) - : await callAsCurrentUser(operationName, params); - } catch (e) { - // catch error and throw after outputting debug info - esError = e; - } - - if (context.params.query._debug) { - const highlightColor = esError ? 'bgRed' : 'inverse'; - const diff = process.hrtime(startTime); - const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; - const routeInfo = `${request.route.method.toUpperCase()} ${ - request.route.path - }`; - - console.log( - chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) - ); - - if (operationName === 'search') { - console.log(`GET ${params.index}/_${operationName}`); - console.log(formatObj(params.body)); - } else { - console.log(chalk.bold('ES operation:'), operationName); - - console.log(chalk.bold('ES query:')); - console.log(formatObj(params)); - } - console.log(`\n`); - } - - if (esError) { - throw esError; - } - - return res; - } - - return { - search: async < - TDocument = unknown, - TSearchRequest extends ESSearchRequest = {} - >( - params: TSearchRequest, - apmOptions?: APMOptions - ): Promise> => { - const nextParams = await getParamsForSearchRequest({ - context, - params, - indices, - includeFrozen, - ...apmOptions, - }); - - return callEs('search', nextParams); - }, - index: (params: APMIndexDocumentParams) => { - return callEs('index', params); - }, - delete: ( - params: Omit - ): Promise => { - return callEs('delete', params); - }, - indicesCreate: (params: IndicesCreateParams) => { - return callEs('indices.create', params); - }, - hasPrivileges: ( - params: IndexPrivilegesParams - ): Promise => { - return callEs('transport.request', { - method: 'POST', - path: '/_security/user/_has_privileges', - body: params, - }); - }, - }; -} diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index 5a4bc62b87486..d8dbd8273f476 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -7,6 +7,8 @@ import { setupRequest } from './setup_request'; import { APMConfig } from '../..'; import { APMRequestHandlerContext } from '../../routes/typings'; import { KibanaRequest } from '../../../../../../src/core/server'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; jest.mock('../settings/apm_indices/get_apm_indices', () => ({ getApmIndices: async () => ({ @@ -93,163 +95,175 @@ function getMockRequest() { } describe('setupRequest', () => { - it('should call callWithRequest with default args', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ index: 'apm-*', body: { foo: 'bar' } } as any); - expect( - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser - ).toHaveBeenCalledWith('search', { - index: 'apm-*', - body: { - foo: 'bar', - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }, - ignore_throttled: true, - }); - }); - - it('should call callWithInternalUser with default args', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { internalClient } = await setupRequest(mockContext, mockRequest); - await internalClient.search({ - index: 'apm-*', - body: { foo: 'bar' }, - } as any); - expect( - mockContext.core.elasticsearch.legacy.client.callAsInternalUser - ).toHaveBeenCalledWith('search', { - index: 'apm-*', - body: { - foo: 'bar', - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }, - ignore_throttled: true, - }); - }); - - describe('observer.version_major filter', () => { - describe('if index is apm-*', () => { - it('should merge `observer.version_major` filter with existing boolean filters', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ - index: 'apm-*', - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, - }); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ + describe('with default args', () => { + it('calls callWithRequest', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { events: [ProcessorEvent.transaction] }, + body: { foo: 'bar' }, + }); + expect( + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser + ).toHaveBeenCalledWith('search', { + index: ['apm-*'], + body: { + foo: 'bar', query: { bool: { filter: [ - { term: 'someTerm' }, + { terms: { 'processor.event': ['transaction'] } }, { range: { 'observer.version_major': { gte: 7 } } }, ], }, }, - }); + }, + ignore_throttled: true, }); + }); - it('should add `observer.version_major` filter if none exists', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ index: 'apm-*' }); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }); + it('calls callWithInternalUser', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { internalClient } = await setupRequest(mockContext, mockRequest); + await internalClient.search({ + index: ['apm-*'], + body: { foo: 'bar' }, + } as any); + expect( + mockContext.core.elasticsearch.legacy.client.callAsInternalUser + ).toHaveBeenCalledWith('search', { + index: ['apm-*'], + body: { + foo: 'bar', + }, }); + }); + }); - it('should not add `observer.version_major` filter if `includeLegacyData=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search( - { - index: 'apm-*', - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + describe('with a bool filter', () => { + it('adds a range filter for `observer.version_major` to the existing filter', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, + body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + }); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.body).toEqual({ + query: { + bool: { + filter: [ + { term: 'someTerm' }, + { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, + { range: { 'observer.version_major': { gte: 7 } } }, + ], }, - { - includeLegacyData: true, - } - ); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ - query: { bool: { filter: [{ term: 'someTerm' }] } }, - }); + }, }); }); - it('if index is not an APM index, it should not add `observer.version_major` filter', async () => { + it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => { const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ - index: '.ml-*', - body: { - query: { bool: { filter: [{ term: 'someTerm' }] } }, + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search( + { + apm: { + events: [ProcessorEvent.error], + }, + body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, }, - }); + { + includeLegacyData: true, + } + ); const params = mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock .calls[0][1]; expect(params.body).toEqual({ query: { bool: { - filter: [{ term: 'someTerm' }], + filter: [ + { term: 'someTerm' }, + { + terms: { + [PROCESSOR_EVENT]: ['error'], + }, + }, + ], }, }, }); }); }); +}); - describe('ignore_throttled', () => { - it('should set `ignore_throttled=true` if `includeFrozen=false`', async () => { - const { mockContext, mockRequest } = getMockRequest(); +describe('without a bool filter', () => { + it('adds a range filter for `observer.version_major`', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + }); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.body).toEqual({ + query: { + bool: { + filter: [ + { terms: { [PROCESSOR_EVENT]: ['error'] } }, + { range: { 'observer.version_major': { gte: 7 } } }, + ], + }, + }, + }); + }); +}); - // mock includeFrozen to return false - mockContext.core.uiSettings.client.get.mockResolvedValue(false); +describe('with includeFrozen=false', () => { + it('sets `ignore_throttled=true`', async () => { + const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); + // mock includeFrozen to return false + mockContext.core.uiSettings.client.get.mockResolvedValue(false); - await client.search({}); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.ignore_throttled).toBe(true); + await apmEventClient.search({ + apm: { + events: [], + }, }); - it('should set `ignore_throttled=false` if `includeFrozen=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.ignore_throttled).toBe(true); + }); +}); - // mock includeFrozen to return true - mockContext.core.uiSettings.client.get.mockResolvedValue(true); +describe('with includeFrozen=true', () => { + it('sets `ignore_throttled=false`', async () => { + const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); + // mock includeFrozen to return true + mockContext.core.uiSettings.client.get.mockResolvedValue(true); - await client.search({}); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.ignore_throttled).toBe(false); + await apmEventClient.search({ + apm: { events: [] }, }); + + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.ignore_throttled).toBe(false); }); }); diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 6f381d4945ab4..ddad2eb2d22dc 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -13,11 +13,17 @@ import { ApmIndicesConfig, } from '../settings/apm_indices/get_apm_indices'; import { ESFilter } from '../../../typings/elasticsearch'; -import { ESClient } from './es_client'; import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; import { APMRequestHandlerContext } from '../../routes/typings'; -import { getESClient } from './es_client'; import { ProcessorEvent } from '../../../common/processor_event'; +import { + APMEventClient, + createApmEventClient, +} from './create_es_client/create_apm_event_client'; +import { + APMInternalClient, + createInternalESClient, +} from './create_es_client/create_internal_es_client'; function decodeUiFilters(uiFiltersEncoded?: string) { if (!uiFiltersEncoded) { @@ -30,8 +36,8 @@ function decodeUiFilters(uiFiltersEncoded?: string) { // https://github.com/microsoft/TypeScript/issues/34933 export interface Setup { - client: ESClient; - internalClient: ESClient; + apmEventClient: APMEventClient; + internalClient: APMInternalClient; ml?: ReturnType; config: APMConfig; indices: ApmIndicesConfig; @@ -78,22 +84,19 @@ export async function setupRequest( context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), ]); - const createClientOptions = { - indices, - includeFrozen, - }; - const uiFiltersES = decodeUiFilters(query.uiFilters); const coreSetupRequest = { indices, - client: getESClient(context, request, { - clientAsInternalUser: false, - ...createClientOptions, + apmEventClient: createApmEventClient({ + context, + request, + indices, + options: { includeFrozen }, }), - internalClient: getESClient(context, request, { - clientAsInternalUser: true, - ...createClientOptions, + internalClient: createInternalESClient({ + context, + request, }), ml: getMlSetup(context, request), config, diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index ee03e77de3580..cb30c6c064848 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -11,7 +11,10 @@ import { IIndexPattern, } from '../../../../../../src/plugins/data/server'; import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { + ProcessorEvent, + UIProcessorEvent, +} from '../../../common/processor_event'; import { APMRequestHandlerContext } from '../../routes/typings'; const cache = new LRU({ @@ -27,7 +30,7 @@ export const getDynamicIndexPattern = async ({ }: { context: APMRequestHandlerContext; indices: ApmIndicesConfig; - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; }) => { const patternIndices = getPatternIndices(indices, processorEvent); const indexPatternTitle = patternIndices.join(','); @@ -75,17 +78,17 @@ export const getDynamicIndexPattern = async ({ function getPatternIndices( indices: ApmIndicesConfig, - processorEvent?: ProcessorEvent + processorEvent?: UIProcessorEvent ) { const indexNames = processorEvent ? [processorEvent] - : ['transaction' as const, 'metric' as const, 'error' as const]; + : [ProcessorEvent.transaction, ProcessorEvent.metric, ProcessorEvent.error]; const indicesMap = { - transaction: indices['apm_oss.transactionIndices'], - metric: indices['apm_oss.metricsIndices'], - error: indices['apm_oss.errorIndices'], + [ProcessorEvent.transaction]: indices['apm_oss.transactionIndices'], + [ProcessorEvent.metric]: indices['apm_oss.metricsIndices'], + [ProcessorEvent.error]: indices['apm_oss.errorIndices'], }; - return indexNames.map((name) => indicesMap[name]); + return indexNames.map((name) => indicesMap[name as UIProcessorEvent]); } diff --git a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap index d8119ac96a536..b88c90a213c67 100644 --- a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`metrics queries with a service node name fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -66,11 +71,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -95,12 +95,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -155,11 +159,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -189,12 +188,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -251,11 +254,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -290,12 +288,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -350,11 +352,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -384,12 +381,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -434,11 +435,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -468,12 +464,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -538,11 +538,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -573,12 +568,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -633,11 +632,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -673,12 +667,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -735,11 +733,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -780,12 +773,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -840,11 +837,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -880,12 +872,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -930,11 +926,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -970,12 +961,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -1040,11 +1035,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1064,12 +1054,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -1124,11 +1118,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1153,12 +1142,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -1215,11 +1208,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1249,12 +1237,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -1309,11 +1301,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1338,12 +1325,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -1388,11 +1379,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1417,6 +1403,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 3ed6e4a944b51..e5c573ba1ec02 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -18,8 +18,8 @@ import { } from '../../../../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; import { ChartBase } from '../../../types'; -import { getMetricsProjection } from '../../../../../../common/projections/metrics'; -import { mergeProjection } from '../../../../../../common/projections/util/merge_projection'; +import { getMetricsProjection } from '../../../../../projections/metrics'; +import { mergeProjection } from '../../../../../projections/util/merge_projection'; import { AGENT_NAME, LABEL_NAME, @@ -42,7 +42,7 @@ export async function fetchAndTransformGcMetrics({ chartBase: ChartBase; fieldName: typeof METRIC_JAVA_GC_COUNT | typeof METRIC_JAVA_GC_TIME; }) { - const { start, end, client } = setup; + const { start, end, apmEventClient } = setup; const { bucketSize } = getBucketSize(start, end, 'auto'); @@ -105,7 +105,7 @@ export async function fetchAndTransformGcMetrics({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); const { aggregations } = response; diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 895920a9b6c7d..f6e201b395c37 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -5,7 +5,6 @@ */ import { Unionize, Overwrite } from 'utility-types'; -import { ESSearchRequest } from '../../../typings/elasticsearch'; import { Setup, SetupTimeRange, @@ -14,9 +13,10 @@ import { import { getMetricsDateHistogramParams } from '../helpers/metrics'; import { ChartBase } from './types'; import { transformDataToMetricsChart } from './transform_metrics_chart'; -import { getMetricsProjection } from '../../../common/projections/metrics'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getMetricsProjection } from '../../projections/metrics'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; +import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; type MetricsAggregationMap = Unionize<{ min: AggregationOptionsByType['min']; @@ -28,7 +28,7 @@ type MetricsAggregationMap = Unionize<{ type MetricAggs = Record; export type GenericMetricsRequest = Overwrite< - ESSearchRequest, + APMEventESSearchRequest, { body: { aggs: { @@ -65,7 +65,7 @@ export async function fetchAndTransformMetrics({ aggs: T; additionalFilters?: Filter[]; }) { - const { start, end, client } = setup; + const { start, end, apmEventClient } = setup; const projection = getMetricsProjection({ setup, @@ -91,7 +91,7 @@ export async function fetchAndTransformMetrics({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); return transformDataToMetricsChart(response, chartBase); } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts index 4c4d058c7139d..8a1f3cb0e0149 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts @@ -6,10 +6,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; -import { - SERVICE_NAME, - PROCESSOR_EVENT, -} from '../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export async function getServiceCount({ @@ -17,36 +14,27 @@ export async function getServiceCount({ }: { setup: Setup & SetupTimeRange; }) { - const { client, indices, start, end } = setup; + const { apmEventClient, start, end } = setup; const params = { - index: [ - indices['apm_oss.transactionIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { range: rangeFilter(start, end) }, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.error, - ProcessorEvent.transaction, - ProcessorEvent.metric, - ], - }, - }, - ], + filter: [{ range: rangeFilter(start, end) }], }, }, aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } }, }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); return aggregations?.serviceCount.value || 0; } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts index 0d1a4274c16dc..116b37a395299 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts @@ -10,7 +10,6 @@ */ import { rangeFilter } from '../../../common/utils/range_filter'; import { Coordinates } from '../../../../observability/public'; -import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ProcessorEvent } from '../../../common/processor_event'; @@ -21,18 +20,17 @@ export async function getTransactionCoordinates({ setup: Setup & SetupTimeRange; bucketSize: string; }): Promise { - const { client, indices, start, end } = setup; + const { apmEventClient, start, end } = setup; - const { aggregations } = await client.search({ - index: indices['apm_oss.transactionIndices'], + const { aggregations } = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - { range: rangeFilter(start, end) }, - ], + filter: [{ range: rangeFilter(start, end) }], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts index fc7445ab4a225..66d82b9f88355 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts @@ -3,41 +3,27 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; export async function hasData({ setup }: { setup: Setup }) { - const { client, indices } = setup; + const { apmEventClient } = setup; try { const params = { - index: [ - indices['apm_oss.transactionIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, terminateAfter: 1, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.error, - ProcessorEvent.metric, - ProcessorEvent.transaction, - ], - }, - }, - ], - }, - }, }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); return response.hits.total.value > 0; } catch (e) { return false; diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index 602eb88ba8940..c5264373ea495 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`rum client dashboard queries fetches client metrics 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "backEnd": Object { @@ -34,11 +39,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -59,12 +59,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches page load distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "durPercentiles": Object { @@ -101,11 +105,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -126,12 +125,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches page view trends 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "pageViews": Object { @@ -154,11 +157,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -179,12 +177,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches rum services 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -206,11 +208,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -231,6 +228,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index 8b3f733fc402a..194c136e2b3d0 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -45,9 +45,9 @@ export async function getClientMetrics({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const { backEnd, domInteractive, pageViews } = response.aggregations!; // Divide by 1000 to convert ms into seconds diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index e847a87264759..2a0c709ea9235 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -57,12 +57,12 @@ export async function getPageLoadDistribution({ }, }); - const { client } = setup; + const { apmEventClient } = setup; const { aggregations, hits: { total }, - } = await client.search(params); + } = await apmEventClient.search(params); if (total.value === 0) { return null; @@ -130,9 +130,9 @@ const getPercentilesDistribution = async ( }, }); - const { client } = setup; + const { apmEventClient } = setup; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const pageDist = aggregations?.loadDistribution.values ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 30b2677d3c217..23169ddaca534 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -56,9 +56,9 @@ export async function getPageViewTrends({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const result = response.aggregations?.pageViews.buckets ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index ea9d701e64c3d..ffb06e649b9be 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -16,6 +17,7 @@ import { USER_AGENT_DEVICE, USER_AGENT_NAME, USER_AGENT_OS, + TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; import { MICRO_TO_SEC, microToSec } from './get_page_load_distribution'; @@ -53,11 +55,11 @@ export const getPageLoadDistBreakdown = async ( }); const params = mergeProjection(projection, { + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, - query: { - bool: projection.body.query.bool, - }, aggs: { breakdowns: { terms: { @@ -67,7 +69,7 @@ export const getPageLoadDistBreakdown = async ( aggs: { page_dist: { percentile_ranks: { - field: 'transaction.duration.us', + field: TRANSACTION_DURATION, values: stepValues, keyed: false, hdr: { @@ -81,9 +83,9 @@ export const getPageLoadDistBreakdown = async ( }, }); - const { client } = setup; + const { apmEventClient } = setup; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const pageDistBreakdowns = aggregations?.breakdowns.buckets; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts index 5957a25239307..9bfa109f00faf 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; export async function getRumServices({ setup, @@ -38,9 +38,9 @@ export async function getRumServices({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const result = response.aggregations?.services.buckets ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index a14affb6eeec5..3681923b484b0 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -55,9 +55,9 @@ export async function getVisitorBreakdown({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const { browsers, os, devices } = response.aggregations!; return { diff --git a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts index 08c8aba5f0207..14047f4bacea9 100644 --- a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts @@ -3,10 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - PROCESSOR_EVENT, - TRACE_ID, -} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { TRACE_ID } from '../../../common/elasticsearch_fieldnames'; import { ConnectionNode, ExternalConnectionNode, @@ -18,23 +16,17 @@ export async function fetchServicePathsFromTraceIds( setup: Setup, traceIds: string[] ) { - const { indices, client } = setup; + const { apmEventClient } = setup; const serviceMapParams = { - index: [ - indices['apm_oss.spanIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ - { - terms: { - [PROCESSOR_EVENT]: ['span', 'transaction'], - }, - }, { terms: { [TRACE_ID]: traceIds, @@ -212,7 +204,7 @@ export async function fetchServicePathsFromTraceIds( }, }; - const serviceMapFromTraceIdsScriptResponse = await client.search( + const serviceMapFromTraceIdsScriptResponse = await apmEventClient.search( serviceMapParams ); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index cd125f944f8a5..b162c3b61d928 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -10,8 +10,8 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getServicesProjection } from '../../../common/projections/services'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getServicesProjection } from '../../projections/services'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { PromiseReturnType } from '../../../typings/common'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { transformServiceMapResponses } from './transform_service_map_responses'; @@ -118,9 +118,9 @@ async function getServicesData(options: IEnvOptions) { }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); return ( response.aggregations?.services.buckets.map((bucket) => { 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 1e0d001340edf..d1c99d778c8f0 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 @@ -12,7 +12,7 @@ describe('getServiceMapServiceNodeInfo', () => { describe('with no results', () => { it('returns null data', async () => { const setup = ({ - client: { + apmEventClient: { search: () => Promise.resolve({ hits: { total: { value: 0 } }, @@ -49,7 +49,7 @@ describe('getServiceMapServiceNodeInfo', () => { }); const setup = ({ - client: { + apmEventClient: { search: () => Promise.resolve({ hits: { total: { value: 1 } }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 0f7136d6d74a4..330d38739a063 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -6,13 +6,12 @@ import { UIFilters } from '../../../typings/ui_filters'; import { + SERVICE_NAME, + TRANSACTION_DURATION, TRANSACTION_TYPE, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; @@ -109,17 +108,18 @@ async function getTransactionStats({ avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; }> { - const { indices, client } = setup; + const { apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ ...filter, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { terms: { [TRANSACTION_TYPE]: [ @@ -135,8 +135,9 @@ async function getTransactionStats({ aggs: { duration: { avg: { field: TRANSACTION_DURATION } } }, }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); const docCount = response.hits.total.value; + return { avgTransactionDuration: response.aggregations?.duration.value ?? null, avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null, @@ -147,18 +148,17 @@ async function getCpuStats({ setup, filter, }: TaskParameters): Promise<{ avgCpuUsage: number | null }> { - const { indices, client } = setup; + const { apmEventClient } = setup; - const response = await client.search({ - index: indices['apm_oss.metricsIndices'], + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { bool: { - filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.metric } }, - { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }, - ]), + filter: [...filter, { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }], }, }, aggs: { avgCpuUsage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } } }, @@ -172,17 +172,19 @@ async function getMemoryStats({ setup, filter, }: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { - const { client, indices } = setup; - const response = await client.search({ - index: indices['apm_oss.metricsIndices'], + const { apmEventClient } = setup; + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, body: { query: { bool: { - filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: 'metric' } }, + filter: [ + ...filter, { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - ]), + ], }, }, aggs: { avgMemoryUsage: { avg: { script: percentMemoryUsedScript } } }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 11c3a00f32980..d6d681f24ab85 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { uniq, take, sortBy } from 'lodash'; +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { rangeFilter } from '../../../common/utils/range_filter'; import { ESFilter } from '../../../typings/elasticsearch'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, TRACE_ID, @@ -26,18 +26,13 @@ export async function getTraceSampleIds({ environment?: string; setup: Setup & SetupTimeRange; }) { - const { start, end, client, indices, config } = setup; + const { start, end, apmEventClient, config } = setup; const rangeQuery = { range: rangeFilter(start, end) }; const query = { bool: { filter: [ - { - term: { - [PROCESSOR_EVENT]: 'span', - }, - }, { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE, @@ -67,7 +62,9 @@ export async function getTraceSampleIds({ const samplerShardSize = traceIdBucketSize * 10; const params = { - index: [indices['apm_oss.spanIndices']], + apm: { + events: [ProcessorEvent.span], + }, body: { size: 0, query, @@ -126,9 +123,7 @@ export async function getTraceSampleIds({ }, }; - const tracesSampleResponse = await client.search( - params - ); + const tracesSampleResponse = await apmEventClient.search(params); // make sure at least one trace per composite/connection bucket // is queried diff --git a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap index 3935ecda42db9..87aca0d056909 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`service node queries fetches metadata for a service node 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "containerId": Object { @@ -30,11 +35,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -59,12 +59,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`service node queries fetches metadata for unidentified service nodes 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "containerId": Object { @@ -93,11 +97,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -128,12 +127,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`service node queries fetches services nodes 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nodes": Object { @@ -174,11 +177,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -197,6 +195,5 @@ Object { }, }, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/plugins/apm/server/lib/service_nodes/index.ts index de66c242815a4..a83aba192dba9 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts @@ -9,8 +9,8 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { getServiceNodesProjection } from '../../../common/projections/service_nodes'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; import { METRIC_PROCESS_CPU_PERCENT, @@ -26,7 +26,7 @@ const getServiceNodes = async ({ setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) => { - const { client } = setup; + const { apmEventClient } = setup; const projection = getServiceNodesProjection({ setup, serviceName }); @@ -66,7 +66,7 @@ const getServiceNodes = async ({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); if (!response.aggregations) { return []; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index 0fc1f89a3723b..ca86c1d93fa6e 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -2,48 +2,32 @@ exports[`services queries fetches the agent status 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + "metric", + "sourcemap", + "transaction", + ], + }, "body": Object { - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "error", - "metric", - "sourcemap", - "transaction", - ], - }, - }, - ], - }, - }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - "myIndex", - ], "terminateAfter": 1, } `; exports[`services queries fetches the legacy data status 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - ], - }, - }, Object { "range": Object { "observer.version_major": Object { @@ -56,13 +40,19 @@ Object { }, "size": 0, }, - "index": "myIndex", "terminateAfter": 1, } `; exports[`services queries fetches the service agent name 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + "transaction", + "metric", + ], + }, "body": Object { "aggs": Object { "agents": Object { @@ -80,15 +70,6 @@ Object { "service.name": "foo", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "error", - "transaction", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -103,11 +84,6 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], "terminateAfter": 1, } `; @@ -115,6 +91,11 @@ Object { exports[`services queries fetches the service items 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -148,20 +129,20 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", - "size": 0, }, Object { + "apm": Object { + "events": Array [ + "metric", + "error", + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -198,27 +179,18 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "metric", - "error", - "transaction", - ], - }, - }, ], }, }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -245,19 +217,18 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", }, Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -284,19 +255,20 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", }, Object { + "apm": Object { + "events": Array [ + "metric", + "transaction", + "error", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -330,31 +302,22 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, ], }, }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], }, ] `; exports[`services queries fetches the service transaction types 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "types": Object { @@ -372,13 +335,6 @@ Object { "service.name": "foo", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -393,6 +349,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 6a8aaf8dca8a6..ad3f47d443b87 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { isNumber } from 'lodash'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { Annotation, AnnotationType } from '../../../../common/annotations'; import { SetupTimeRange, Setup } from '../../helpers/setup_request'; import { ESFilter } from '../../../../typings/elasticsearch'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_VERSION, } from '../../../../common/elasticsearch_fieldnames'; @@ -24,23 +24,24 @@ export async function getDerivedServiceAnnotations({ environment?: string; setup: Setup & SetupTimeRange; }) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [SERVICE_NAME]: serviceName } }, ...getEnvironmentUiFilterES(environment), ]; const versions = ( - await client.search({ - index: indices['apm_oss.transactionIndices'], + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: filter.concat({ range: rangeFilter(start, end) }), + filter: [...filter, { range: rangeFilter(start, end) }], }, }, aggs: { @@ -59,17 +60,15 @@ export async function getDerivedServiceAnnotations({ } const annotations = await Promise.all( versions.map(async (version) => { - const response = await client.search({ - index: indices['apm_oss.transactionIndices'], + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: filter.concat({ - term: { - [SERVICE_VERSION]: version, - }, - }), + filter: [...filter, { term: { [SERVICE_VERSION]: version } }], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts index 8d75d746c7fca..a95c27df0e502 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, AGENT_NAME, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; @@ -15,24 +15,23 @@ export async function getServiceAgentName( serviceName: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.error, + ProcessorEvent.transaction, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { - terms: { [PROCESSOR_EVENT]: ['error', 'transaction', 'metric'] }, - }, { range: rangeFilter(start, end) }, ], }, @@ -45,7 +44,7 @@ export async function getServiceAgentName( }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const agentName = aggregations?.agents.buckets[0]?.key as string | undefined; return { agentName }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts index c2d9fa6c1df39..fca472b0ce8c2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts @@ -14,8 +14,8 @@ import { CONTAINER_ID, } from '../../../common/elasticsearch_fieldnames'; import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; -import { getServiceNodesProjection } from '../../../common/projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; export async function getServiceNodeMetadata({ serviceName, @@ -26,7 +26,7 @@ export async function getServiceNodeMetadata({ serviceNodeName: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { client } = setup; + const { apmEventClient } = setup; const query = mergeProjection( getServiceNodesProjection({ @@ -55,7 +55,7 @@ export async function getServiceNodeMetadata({ } ); - const response = await client.search(query); + const response = await apmEventClient.search(query); return { host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts index d88be4055dc21..6c6e03ab0b46f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; @@ -15,17 +15,18 @@ export async function getServiceTransactionTypes( serviceName: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, { range: rangeFilter(start, end) }, ], }, @@ -38,7 +39,7 @@ export async function getServiceTransactionTypes( }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const transactionTypes = aggregations?.types.buckets.map((bucket) => bucket.key as string) || []; return { transactionTypes }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts index dde726c51393f..1be95967cb47a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts @@ -4,33 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - OBSERVER_VERSION_MAJOR, - PROCESSOR_EVENT, -} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { OBSERVER_VERSION_MAJOR } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../helpers/setup_request'; // returns true if 6.x data is found export async function getLegacyDataStatus(setup: Setup) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: [ - { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, - { range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }, - ], + filter: [{ range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }], }, }, }, }; - const resp = await client.search(params, { includeLegacyData: true }); + const resp = await apmEventClient.search(params, { includeLegacyData: true }); const hasLegacyData = resp.hits.total.value > 0; return hasLegacyData; } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 14772e77fe1c2..d888b43b63fac 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -10,7 +10,7 @@ import { SetupTimeRange, SetupUIFilters, } from '../../helpers/setup_request'; -import { getServicesProjection } from '../../../../common/projections/services'; +import { getServicesProjection } from '../../../projections/services'; import { getTransactionDurationAverages, getAgentNames, @@ -25,7 +25,7 @@ export type ServicesItemsProjection = ReturnType; export async function getServicesItems(setup: ServicesItemsSetup) { const params = { - projection: getServicesProjection({ setup, noEvents: true }), + projection: getServicesProjection({ setup }), setup, }; 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 de699028f5675..ddce3b667a603 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 @@ -5,12 +5,11 @@ */ import { - PROCESSOR_EVENT, TRANSACTION_DURATION, AGENT_NAME, SERVICE_ENVIRONMENT, } from '../../../../common/elasticsearch_fieldnames'; -import { mergeProjection } from '../../../../common/projections/util/merge_projection'; +import { mergeProjection } from '../../../projections/util/merge_projection'; import { ProcessorEvent } from '../../../../common/processor_event'; import { ServicesItemsSetup, @@ -31,22 +30,15 @@ export const getTransactionDurationAverages = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; + const { apmEventClient } = setup; - const response = await client.search( + const response = await apmEventClient.search( mergeProjection(projection, { - size: 0, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { - query: { - bool: { - filter: projection.body.query.bool.filter.concat({ - term: { - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - }, - }), - }, - }, + size: 0, aggs: { services: { terms: { @@ -82,32 +74,18 @@ export const getAgentNames = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.metric, + ProcessorEvent.error, + ProcessorEvent.transaction, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.metric, - ProcessorEvent.error, - ProcessorEvent.transaction, - ], - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -136,11 +114,7 @@ export const getAgentNames = async ({ return aggregations.services.buckets.map((bucket) => ({ serviceName: bucket.key as string, - agentName: (bucket.agent_name.hits.hits[0]?._source as { - agent: { - name: string; - }; - }).agent.name, + agentName: bucket.agent_name.hits.hits[0]?._source.agent.name, })); }; @@ -148,24 +122,14 @@ export const getTransactionRates = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -199,24 +163,14 @@ export const getErrorRates = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [PROCESSOR_EVENT]: ProcessorEvent.error, - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -250,32 +204,18 @@ export const getEnvironments = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.metric, + ProcessorEvent.transaction, + ProcessorEvent.error, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.transaction, - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - }, - ], - }, - }, aggs: { services: { terms: { diff --git a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts index 42f53fc93fa60..eed9f2588152d 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts @@ -4,43 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; // Note: this logic is duplicated in tutorials/apm/envs/on_prem export async function hasHistoricalAgentData(setup: Setup) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - indices['apm_oss.sourcemapIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.error, + ProcessorEvent.metric, + ProcessorEvent.sourcemap, + ProcessorEvent.transaction, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { - [PROCESSOR_EVENT]: [ - 'error', - 'metric', - 'sourcemap', - 'transaction', - ], - }, - }, - ], - }, - }, }, }; - const resp = await client.search(params); - const hasHistorialAgentData = resp.hits.total.value > 0; - return hasHistorialAgentData; + const resp = await apmEventClient.search(params); + return resp.hits.total.value > 0; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 24a1840bc0ab8..2b465a0f87475 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -115,6 +115,13 @@ Object { exports[`agent configuration queries getServiceNames fetches service names 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -124,28 +131,8 @@ Object { }, }, }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, - ], - }, - }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts index 4d61e1e9ae284..86aeb95e165a0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -10,7 +10,7 @@ import { AgentConfiguration, AgentConfigurationIntake, } from '../../../../common/agent_configuration/configuration_types'; -import { APMIndexDocumentParams } from '../../helpers/es_client'; +import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; export async function createOrUpdateConfiguration({ configurationId, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts index 39674ee57abf6..9f0e65d492a8f 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { AGENT_NAME } from '../../../../common/elasticsearch_fieldnames'; export async function getAgentNameByService({ @@ -18,25 +16,22 @@ export async function getAgentNameByService({ serviceName: string; setup: Setup; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - { term: { [SERVICE_NAME]: serviceName } }, - ], + filter: [{ term: { [SERVICE_NAME]: serviceName } }], }, }, aggs: { @@ -47,7 +42,7 @@ export async function getAgentNameByService({ }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const agentName = aggregations?.agent_names.buckets[0]?.key; return agentName as string | undefined; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 068bb30ddcf79..8b6c1d82beab0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -4,37 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration/all_option'; export type AgentConfigurationServicesAPIResponse = PromiseReturnType< typeof getServiceNames >; export async function getServiceNames({ setup }: { setup: Setup }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -46,7 +37,7 @@ export async function getServiceNames({ setup }: { setup: Setup }) { }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const serviceNames = resp.aggregations?.services.buckets .map((bucket) => bucket.key as string) diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap index a91641b592526..0649c8c38d29a 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap @@ -2,15 +2,15 @@ exports[`custom link get transaction fetches with all filter 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "terms": Object { "service.name": Array [ @@ -43,7 +43,6 @@ Object { }, }, }, - "index": "myIndex", "size": 1, "terminateAfter": 1, } @@ -51,20 +50,18 @@ Object { exports[`custom link get transaction fetches without filter 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { - "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, - ], + "filter": Array [], }, }, }, - "index": "myIndex", "size": 1, "terminateAfter": 1, } diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts index 16a694c04c485..48b115619283c 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts @@ -8,9 +8,9 @@ import { CustomLink, CustomLinkES, } from '../../../../common/custom_link/custom_link_types'; -import { APMIndexDocumentParams } from '../../helpers/es_client'; import { Setup } from '../../helpers/setup_request'; import { toESFormat } from './helper'; +import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; export async function createOrUpdateCustomLink({ customLinkId, diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts index e3becc040580f..9bf489e768a4b 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import * as t from 'io-ts'; -import { PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; import { filterOptionsRt } from './custom_link_types'; @@ -18,7 +16,7 @@ export async function getTransaction({ setup: Setup; filters?: t.TypeOf; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const esFilters = Object.entries(filters) // loops through the filters splitting the value by comma and removing white spaces @@ -32,19 +30,18 @@ export async function getTransaction({ const params = { terminateAfter: 1, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, size: 1, body: { query: { bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...esFilters, - ], + filter: esFilters, }, }, }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap index 0a9f9d38b2be7..3c521839b587e 100644 --- a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`trace queries fetches a trace 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "by_transaction_id": Object { @@ -20,11 +25,6 @@ Object { "trace.id": "foo", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -48,6 +48,5 @@ Object { }, "size": "myIndex", }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index f9374558dfeeb..17f9743ae9f00 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, TRACE_ID, PARENT_ID, TRANSACTION_DURATION, @@ -13,8 +13,6 @@ import { TRANSACTION_ID, ERROR_LOG_LEVEL, } from '../../../common/elasticsearch_fieldnames'; -import { Span } from '../../../typings/es_schemas/ui/span'; -import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; @@ -28,19 +26,20 @@ export async function getTraceItems( traceId: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, config, indices } = setup; + const { start, end, apmEventClient, config } = setup; const maxTraceItems = config['xpack.apm.ui.maxTraceItems']; const excludedLogLevels = ['debug', 'info', 'warning']; - const errorResponsePromise = client.search({ - index: indices['apm_oss.errorIndices'], + const errorResponsePromise = apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, body: { size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { range: rangeFilter(start, end) }, ], must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } }, @@ -59,18 +58,16 @@ export async function getTraceItems( }, }); - const traceResponsePromise = client.search({ - index: [ - indices['apm_oss.spanIndices'], - indices['apm_oss.transactionIndices'], - ], + const traceResponsePromise = apmEventClient.search({ + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, body: { size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { terms: { [PROCESSOR_EVENT]: ['span', 'transaction'] } }, { range: rangeFilter(start, end) }, ], should: { @@ -91,22 +88,17 @@ export async function getTraceItems( // explicit intermediary types to avoid TS "excessively deep" error PromiseValueType, PromiseValueType - // @ts-ignore ] = await Promise.all([errorResponsePromise, traceResponsePromise]); const exceedsMax = traceResponse.hits.total.value > maxTraceItems; - const items = (traceResponse.hits.hits as Array<{ - _source: Transaction | Span; - }>).map((hit) => hit._source); + const items = traceResponse.hits.hits.map((hit) => hit._source); const errorFrequencies: { errorsPerTransaction: ErrorsPerTransaction; errorDocs: APMError[]; } = { - errorDocs: errorResponse.hits.hits.map( - ({ _source }) => _source as APMError - ), + errorDocs: errorResponse.hits.hits.map(({ _source }) => _source), errorsPerTransaction: errorResponse.aggregations?.by_transaction_id.buckets.reduce( (acc, current) => { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index deca46f4ebd0c..0ea7bcf7ce8ab 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -3,6 +3,11 @@ exports[`transaction group queries fetches top traces 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -46,11 +51,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -84,10 +84,14 @@ Array [ }, ], }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -131,11 +135,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -152,10 +151,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -199,11 +202,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -220,7 +218,6 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, ] @@ -229,6 +226,11 @@ Array [ exports[`transaction group queries fetches top transactions 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -257,11 +259,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -298,10 +295,14 @@ Array [ }, ], }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -330,11 +331,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -354,10 +350,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -386,11 +386,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -410,10 +405,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -448,11 +447,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -472,7 +466,6 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, ] diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 73bf1d01924e7..b06d1a8af3bc5 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -7,13 +7,12 @@ import { take, sortBy } from 'lodash'; import { Unionize } from 'utility-types'; import moment from 'moment'; import { joinByKey } from '../../../common/utils/join_by_key'; -import { ESSearchRequest } from '../../../typings/elasticsearch'; import { SERVICE_NAME, TRANSACTION_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getTransactionGroupsProjection } from '../../../common/projections/transaction_groups'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getTransactionGroupsProjection } from '../../projections/transaction_groups'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; import { Transaction } from '../../../typings/es_schemas/ui/transaction'; @@ -45,7 +44,9 @@ export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; -export type TransactionGroupRequestBase = ESSearchRequest & { +export type TransactionGroupRequestBase = ReturnType< + typeof getTransactionGroupsProjection +> & { body: { aggs: { transaction_groups: Unionize< 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 6a1ee8daad7c7..8fb2ceb30db85 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 @@ -5,7 +5,6 @@ */ import { mean } from 'lodash'; import { - PROCESSOR_EVENT, HTTP_RESPONSE_STATUS_CODE, TRANSACTION_NAME, TRANSACTION_TYPE, @@ -31,7 +30,7 @@ export async function getErrorRate({ transactionName?: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const transactionNamefilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] @@ -42,7 +41,6 @@ export async function getErrorRate({ const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { range: rangeFilter(start, end) }, { exists: { field: HTTP_RESPONSE_STATUS_CODE } }, ...transactionNamefilter, @@ -51,7 +49,9 @@ export async function getErrorRate({ ]; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter } }, @@ -68,7 +68,7 @@ export async function getErrorRate({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const noHits = resp.hits.total.value === 0; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index 59fb370113ec2..7d45f39e08a83 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -5,7 +5,6 @@ */ import { merge } from 'lodash'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; -import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { TRANSACTION_SAMPLED, TRANSACTION_DURATION, @@ -52,7 +51,7 @@ export async function getSamples({ request, setup }: MetricParams) { { '@timestamp': { order: 'desc' as const } }, ]; - const response = await setup.client.search({ + const response = await setup.apmEventClient.search({ ...params, body: { ...params.body, @@ -73,7 +72,7 @@ export async function getSamples({ request, setup }: MetricParams) { return { key: bucket.key as BucketKey, count: bucket.doc_count, - sample: bucket.sample.hits.hits[0]._source as Transaction, + sample: bucket.sample.hits.hits[0]._source, }; }); } @@ -87,7 +86,7 @@ export async function getAverages({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] @@ -108,7 +107,7 @@ export async function getSums({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] @@ -131,7 +130,7 @@ export async function getPercentiles({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index cc5900919f829..9bc4b1d69d9ac 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -2,15 +2,15 @@ exports[`transaction queries fetches a transaction 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.id": "foo", @@ -35,12 +35,16 @@ Object { }, "size": 1, }, - "index": "myIndex", } `; exports[`transaction queries fetches breakdown data for transactions 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "by_date": Object { @@ -146,11 +150,6 @@ Object { "transaction.type": "bar", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -170,12 +169,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches breakdown data for transactions for a transaction name 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "by_date": Object { @@ -281,11 +284,6 @@ Object { "transaction.type": "bar", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -310,12 +308,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -376,11 +378,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -405,12 +402,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts for a transaction type 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -471,11 +472,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -505,12 +501,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts for a transaction type and transaction name 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -571,11 +571,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -610,12 +605,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "stats": Object { @@ -632,11 +631,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "baz", @@ -666,6 +660,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts index d8175a34ceb9f..278819ea20a83 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts @@ -15,7 +15,7 @@ describe('fetcher', () => { it('performs a search', async () => { const search = jest.fn(); const setup = ({ - client: { search }, + apmEventClient: { search }, indices: {}, uiFiltersES: [], } as unknown) as Setup & SetupTimeRange & SetupUIFilters; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts index b4d98ec41fc2d..f68082dfaa1e1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -7,7 +7,6 @@ import { ESFilter } from '../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, USER_AGENT_NAME, @@ -23,7 +22,7 @@ import { ProcessorEvent } from '../../../../common/processor_event'; export type ESResponse = PromiseReturnType; export function fetcher(options: Options) { - const { end, client, indices, start, uiFiltersES } = options.setup; + const { end, apmEventClient, start, uiFiltersES } = options.setup; const { serviceName, transactionName } = options; const { intervalString } = getBucketSize(start, end, 'auto'); @@ -32,7 +31,6 @@ export function fetcher(options: Options) { : []; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { range: rangeFilter(start, end) }, @@ -41,7 +39,9 @@ export function fetcher(options: Options) { ]; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter } }, @@ -80,5 +80,5 @@ export function fetcher(options: Options) { }, }; - return client.search(params); + return apmEventClient.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts index ea6213f64ee36..9bb42d2fa7aad 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { CLIENT_GEO_COUNTRY_ISO_CODE, - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_TYPE, @@ -29,12 +29,14 @@ export async function getTransactionAvgDurationByCountry({ serviceName: string; transactionName?: string; }) { - const { uiFiltersES, client, start, end, indices } = setup; + const { uiFiltersES, apmEventClient, start, end } = setup; const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] : []; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { @@ -42,7 +44,6 @@ export async function getTransactionAvgDurationByCountry({ filter: [ { term: { [SERVICE_NAME]: serviceName } }, ...transactionNameFilter, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { exists: { field: CLIENT_GEO_COUNTRY_ISO_CODE } }, { range: rangeFilter(start, end) }, @@ -66,7 +67,7 @@ export async function getTransactionAvgDurationByCountry({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); if (!resp.aggregations) { return []; diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 85d4eab448c72..3c1618ed7715f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -26,7 +26,7 @@ function getMockSetup(esResponse: any) { return { start: 0, end: 500000, - client: { search: clientSpy } as any, + apmEventClient: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, config: new Proxy( {}, diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 3c48c14c2a471..7248399d1f93f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -5,6 +5,7 @@ */ import { flatten, orderBy, last } from 'lodash'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { SERVICE_NAME, SPAN_SUBTYPE, @@ -13,7 +14,6 @@ import { TRANSACTION_TYPE, TRANSACTION_NAME, TRANSACTION_BREAKDOWN_COUNT, - PROCESSOR_EVENT, } from '../../../../common/elasticsearch_fieldnames'; import { Setup, @@ -36,7 +36,7 @@ export async function getTransactionBreakdown({ transactionName?: string; transactionType: string; }) { - const { uiFiltersES, client, start, end, indices } = setup; + const { uiFiltersES, apmEventClient, start, end } = setup; const subAggs = { sum_all_self_times: { @@ -82,7 +82,6 @@ export async function getTransactionBreakdown({ const filters = [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, { range: rangeFilter(start, end) }, ...uiFiltersES, ]; @@ -92,7 +91,9 @@ export async function getTransactionBreakdown({ } const params = { - index: indices['apm_oss.metricsIndices'], + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { @@ -110,7 +111,7 @@ export async function getTransactionBreakdown({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const formatBucket = ( aggs: diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap index 25ebb15fd73e8..7bc60a7fc7f1a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap @@ -4,6 +4,11 @@ exports[`timeseriesFetcher should call client with correct query 1`] = ` Array [ Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -64,11 +69,6 @@ Array [ "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "myServiceName", @@ -98,7 +98,6 @@ Array [ }, "size": 0, }, - "index": "myIndex", }, ], ] diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts index fb357040f5781..09e1287f032f5 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { ESResponse, timeseriesFetcher } from './fetcher'; import { APMConfig } from '../../../../../server'; +import { ProcessorEvent } from '../../../../../common/processor_event'; describe('timeseriesFetcher', () => { let res: ESResponse; @@ -21,7 +21,7 @@ describe('timeseriesFetcher', () => { setup: { start: 1528113600000, end: 1528977600000, - client: { search: clientSpy } as any, + apmEventClient: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, config: new Proxy( {}, @@ -54,15 +54,7 @@ describe('timeseriesFetcher', () => { it('should restrict results to only transaction documents', () => { const query = clientSpy.mock.calls[0][0]; - expect(query.body.query.bool.filter).toEqual( - expect.arrayContaining([ - { - term: { - [PROCESSOR_EVENT]: 'transaction', - }, - } as any, - ]) - ); + expect(query.apm.events).toEqual([ProcessorEvent.transaction]); }); it('should return correct response', () => { diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 8e19af926ce02..1498c22e327d6 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESFilter } from '../../../../../typings/elasticsearch'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_NAME, @@ -34,11 +34,10 @@ export function timeseriesFetcher({ transactionName: string | undefined; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const { intervalString } = getBucketSize(start, end, 'auto'); const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -54,7 +53,9 @@ export function timeseriesFetcher({ } const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 0, query: { bool: { filter } }, @@ -95,5 +96,5 @@ export function timeseriesFetcher({ }, }; - return client.search(params); + return apmEventClient.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts index 3f8bf635712be..bfe72bf7c00f9 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { ProcessorEvent } from '../../../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRACE_ID, TRANSACTION_DURATION, @@ -32,17 +31,18 @@ export async function bucketFetcher( bucketSize: number, setup: Setup & SetupTimeRange & SetupUIFilters ) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, { range: rangeFilter(start, end) }, @@ -85,7 +85,7 @@ export async function bucketFetcher( }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); return response; } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts index 8289113fddae9..139dac3df1171 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_NAME, @@ -23,17 +23,18 @@ export async function getDistributionMax( transactionType: string, setup: Setup & SetupTimeRange & SetupUIFilters ) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, { @@ -59,6 +60,6 @@ export async function getDistributionMax( }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return resp.aggregations ? resp.aggregations.stats.max : null; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index a7de93a3bf650..9aa1a8f4de87f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -5,11 +5,9 @@ */ import { - PROCESSOR_EVENT, TRACE_ID, TRANSACTION_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, @@ -27,16 +25,17 @@ export async function getTransaction({ traceId: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; - const params = { - index: indices['apm_oss.transactionIndices'], + const resp = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 1, query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [TRANSACTION_ID]: transactionId } }, { term: { [TRACE_ID]: traceId } }, { range: rangeFilter(start, end) }, @@ -44,8 +43,7 @@ export async function getTransaction({ }, }, }, - }; + }); - const resp = await client.search(params); return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts index ad4f58d85d188..8ba61c6c726a4 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts @@ -5,11 +5,9 @@ */ import { - PROCESSOR_EVENT, TRACE_ID, PARENT_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; @@ -17,9 +15,12 @@ export async function getRootTransactionByTraceId( traceId: string, setup: Setup ) { - const { client, indices } = setup; + const { apmEventClient } = setup; + const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 1, query: { @@ -35,16 +36,13 @@ export async function getRootTransactionByTraceId( }, }, ], - filter: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ], + filter: [{ term: { [TRACE_ID]: traceId } }], }, }, }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return { transaction: resp.hits.hits[0]?._source, }; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap index 30e75f46ad5e7..d94b766aee6a8 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap @@ -2,6 +2,13 @@ exports[`ui filter queries fetches environments 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -14,15 +21,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -42,16 +40,18 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; exports[`ui filter queries fetches environments without a service name 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -64,15 +64,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -87,10 +78,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts index 3fca30634be6a..98f00bf8e6555 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; @@ -18,12 +18,9 @@ export async function getEnvironments( setup: Setup & SetupTimeRange, serviceName?: string ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; - const filter: ESFilter[] = [ - { terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } }, - { range: rangeFilter(start, end) }, - ]; + const filter: ESFilter[] = [{ range: rangeFilter(start, end) }]; if (serviceName) { filter.push({ @@ -32,11 +29,13 @@ export async function getEnvironments( } const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, body: { size: 0, query: { @@ -55,7 +54,7 @@ export async function getEnvironments( }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const aggs = resp.aggregations; const environmentsBuckets = aggs?.environments.buckets || []; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap index e6b6a9a52adfe..5f38432719280 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap @@ -2,6 +2,13 @@ exports[`local ui filter queries fetches local ui filter aggregations 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "by_terms": Object { @@ -28,15 +35,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -56,10 +54,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts index e892284fd87cd..cfbd79d37c041 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts @@ -5,8 +5,8 @@ */ import { omit } from 'lodash'; -import { mergeProjection } from '../../../../common/projections/util/merge_projection'; -import { Projection } from '../../../../common/projections/typings'; +import { mergeProjection } from '../../../projections/util/merge_projection'; +import { Projection } from '../../../projections/typings'; import { UIFilters } from '../../../../typings/ui_filters'; import { getUiFiltersES } from '../../helpers/convert_ui_filters/get_ui_filters_es'; import { localUIFilters, LocalUIFilterName } from './config'; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts index 3833b93c8d1f7..12c02679d0859 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts @@ -5,7 +5,7 @@ */ import { cloneDeep, orderBy } from 'lodash'; import { UIFilters } from '../../../../typings/ui_filters'; -import { Projection } from '../../../../common/projections/typings'; +import { Projection } from '../../../projections/typings'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { getLocalFilterQuery } from './get_local_filter_query'; import { Setup } from '../../helpers/setup_request'; @@ -26,7 +26,7 @@ export async function getLocalUIFilters({ uiFilters: UIFilters; localFilterNames: LocalUIFilterName[]; }) { - const { client } = setup; + const { apmEventClient } = setup; const projectionWithoutAggs = cloneDeep(projection); @@ -40,7 +40,7 @@ export async function getLocalUIFilters({ localUIFilterName: name, }); - const response = await client.search(query); + const response = await apmEventClient.search(query); const filter = localUIFilters[name]; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts index ac61910968850..92ee67de49314 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts @@ -9,7 +9,7 @@ import { SearchParamsMock, inspectSearchParams, } from '../../../../public/utils/testHelpers'; -import { getServicesProjection } from '../../../../common/projections/services'; +import { getServicesProjection } from '../../../projections/services'; describe('local ui filter queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/common/projections/errors.ts b/x-pack/plugins/apm/server/projections/errors.ts similarity index 70% rename from x-pack/plugins/apm/common/projections/errors.ts rename to x-pack/plugins/apm/server/projections/errors.ts index 390a8a0968102..49a0e9f479d26 100644 --- a/x-pack/plugins/apm/common/projections/errors.ts +++ b/x-pack/plugins/apm/server/projections/errors.ts @@ -8,15 +8,13 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { - PROCESSOR_EVENT, SERVICE_NAME, ERROR_GROUP_ID, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getErrorGroupsProjection({ setup, @@ -25,16 +23,17 @@ export function getErrorGroupsProjection({ setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; return { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error as const], + }, body: { query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { range: rangeFilter(start, end) }, ...uiFiltersES, ], diff --git a/x-pack/plugins/apm/common/projections/metrics.ts b/x-pack/plugins/apm/server/projections/metrics.ts similarity index 72% rename from x-pack/plugins/apm/common/projections/metrics.ts rename to x-pack/plugins/apm/server/projections/metrics.ts index 45998bfe82e96..eb80a6bc73248 100644 --- a/x-pack/plugins/apm/common/projections/metrics.ts +++ b/x-pack/plugins/apm/server/projections/metrics.ts @@ -8,16 +8,14 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, - PROCESSOR_EVENT, SERVICE_NODE_NAME, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; -import { SERVICE_NODE_NAME_MISSING } from '../service_nodes'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { SERVICE_NODE_NAME_MISSING } from '../../common/service_nodes'; +import { ProcessorEvent } from '../../common/processor_event'; function getServiceNodeNameFilters(serviceNodeName?: string) { if (!serviceNodeName) { @@ -40,18 +38,19 @@ export function getMetricsProjection({ serviceName: string; serviceNodeName?: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, { range: rangeFilter(start, end) }, ...getServiceNodeNameFilters(serviceNodeName), ...uiFiltersES, ]; return { - index: indices['apm_oss.metricsIndices'], + apm: { + events: [ProcessorEvent.metric], + }, body: { query: { bool: { diff --git a/x-pack/plugins/apm/common/projections/rum_overview.ts b/x-pack/plugins/apm/server/projections/rum_overview.ts similarity index 71% rename from x-pack/plugins/apm/common/projections/rum_overview.ts rename to x-pack/plugins/apm/server/projections/rum_overview.ts index b1218546d09ff..4588ec2a0451f 100644 --- a/x-pack/plugins/apm/common/projections/rum_overview.ts +++ b/x-pack/plugins/apm/server/projections/rum_overview.ts @@ -8,22 +8,21 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { PROCESSOR_EVENT, TRANSACTION_TYPE } from '../elasticsearch_fieldnames'; -import { rangeFilter } from '../utils/range_filter'; +import { TRANSACTION_TYPE } from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getRumOverviewProjection({ setup, }: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const bool = { filter: [ { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: 'page-load' } }, { // Adding this filter to cater for some inconsistent rum data @@ -36,7 +35,9 @@ export function getRumOverviewProjection({ }; return { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { query: { bool, diff --git a/x-pack/plugins/apm/common/projections/service_nodes.ts b/x-pack/plugins/apm/server/projections/service_nodes.ts similarity index 88% rename from x-pack/plugins/apm/common/projections/service_nodes.ts rename to x-pack/plugins/apm/server/projections/service_nodes.ts index 1bc68f51a26ed..87fe815a12d0d 100644 --- a/x-pack/plugins/apm/common/projections/service_nodes.ts +++ b/x-pack/plugins/apm/server/projections/service_nodes.ts @@ -8,9 +8,8 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { SERVICE_NODE_NAME } from '../elasticsearch_fieldnames'; +import { SERVICE_NODE_NAME } from '../../common/elasticsearch_fieldnames'; import { mergeProjection } from './util/merge_projection'; import { getMetricsProjection } from './metrics'; diff --git a/x-pack/plugins/apm/server/projections/services.ts b/x-pack/plugins/apm/server/projections/services.ts new file mode 100644 index 0000000000000..18fa79f31d6f3 --- /dev/null +++ b/x-pack/plugins/apm/server/projections/services.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Setup, + SetupUIFilters, + SetupTimeRange, +} from '../../server/lib/helpers/setup_request'; +import { SERVICE_NAME } from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; + +export function getServicesProjection({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { start, end, uiFiltersES } = setup; + + return { + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [{ range: rangeFilter(start, end) }, ...uiFiltersES], + }, + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + }, + }, + }, + }, + }; +} diff --git a/x-pack/plugins/apm/common/projections/transaction_groups.ts b/x-pack/plugins/apm/server/projections/transaction_groups.ts similarity index 86% rename from x-pack/plugins/apm/common/projections/transaction_groups.ts rename to x-pack/plugins/apm/server/projections/transaction_groups.ts index 1708d89aad4ec..8aa085cccf82a 100644 --- a/x-pack/plugins/apm/common/projections/transaction_groups.ts +++ b/x-pack/plugins/apm/server/projections/transaction_groups.ts @@ -8,10 +8,11 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { TRANSACTION_NAME, PARENT_ID } from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { + TRANSACTION_NAME, + PARENT_ID, +} from '../../common/elasticsearch_fieldnames'; import { Options } from '../../server/lib/transaction_groups/fetcher'; import { getTransactionsProjection } from './transactions'; import { mergeProjection } from './util/merge_projection'; diff --git a/x-pack/plugins/apm/common/projections/transactions.ts b/x-pack/plugins/apm/server/projections/transactions.ts similarity index 76% rename from x-pack/plugins/apm/common/projections/transactions.ts rename to x-pack/plugins/apm/server/projections/transactions.ts index b6cd73ca9aaad..f428a76a8b0cb 100644 --- a/x-pack/plugins/apm/common/projections/transactions.ts +++ b/x-pack/plugins/apm/server/projections/transactions.ts @@ -8,16 +8,14 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, TRANSACTION_TYPE, - PROCESSOR_EVENT, TRANSACTION_NAME, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getTransactionsProjection({ setup, @@ -30,7 +28,7 @@ export function getTransactionsProjection({ transactionName?: string; transactionType?: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] @@ -45,7 +43,6 @@ export function getTransactionsProjection({ const bool = { filter: [ { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, ...transactionNameFilter, ...transactionTypeFilter, ...serviceNameFilter, @@ -54,7 +51,9 @@ export function getTransactionsProjection({ }; return { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { query: { bool, diff --git a/x-pack/plugins/apm/common/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts similarity index 56% rename from x-pack/plugins/apm/common/projections/typings.ts rename to x-pack/plugins/apm/server/projections/typings.ts index 693795b09e1d0..77a5beaf54605 100644 --- a/x-pack/plugins/apm/common/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchRequest, ESSearchBody } from '../../typings/elasticsearch'; +import { ESSearchBody } from '../../typings/elasticsearch'; import { AggregationOptionsByType, AggregationInputMap, } from '../../typings/elasticsearch/aggregations'; +import { APMEventESSearchRequest } from '../lib/helpers/create_es_client/create_apm_event_client'; -export type Projection = Omit & { +export type Projection = Omit & { body: Omit & { aggs?: { [key: string]: { @@ -20,14 +21,3 @@ export type Projection = Omit & { }; }; }; - -export enum PROJECTION { - SERVICES = 'services', - TRANSACTION_GROUPS = 'transactionGroups', - TRACES = 'traces', - TRANSACTIONS = 'transactions', - METRICS = 'metrics', - ERROR_GROUPS = 'errorGroups', - SERVICE_NODES = 'serviceNodes', - RUM_OVERVIEW = 'rumOverview', -} diff --git a/x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts similarity index 73% rename from x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts rename to x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts index 33727fcb9c735..aa02c8898d218 100644 --- a/x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts @@ -10,10 +10,19 @@ describe('mergeProjection', () => { it('overrides arrays', () => { expect( mergeProjection( - { body: { query: { bool: { must: [{ terms: ['a'] }] } } } }, - { body: { query: { bool: { must: [{ term: 'b' }] } } } } + { + apm: { events: [] }, + body: { query: { bool: { must: [{ terms: ['a'] }] } } }, + }, + { + apm: { events: [] }, + body: { query: { bool: { must: [{ term: 'b' }] } } }, + } ) ).toEqual({ + apm: { + events: [], + }, body: { query: { bool: { @@ -32,8 +41,11 @@ describe('mergeProjection', () => { const termsAgg = { terms: { field: 'bar' } }; expect( mergeProjection( - { body: { query: {}, aggs: { foo: termsAgg } } }, + { apm: { events: [] }, body: { query: {}, aggs: { foo: termsAgg } } }, { + apm: { + events: [], + }, body: { aggs: { foo: { ...termsAgg, aggs: { bar: { terms: { field: 'baz' } } } }, @@ -42,6 +54,9 @@ describe('mergeProjection', () => { } ) ).toEqual({ + apm: { + events: [], + }, body: { query: {}, aggs: { diff --git a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts similarity index 82% rename from x-pack/plugins/apm/common/projections/util/merge_projection/index.ts rename to x-pack/plugins/apm/server/projections/util/merge_projection/index.ts index 9dc1c815bf169..ea7267dd337c2 100644 --- a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts @@ -6,15 +6,13 @@ import { mergeWith, isPlainObject, cloneDeep } from 'lodash'; import { DeepPartial } from 'utility-types'; import { AggregationInputMap } from '../../../../typings/elasticsearch/aggregations'; -import { - ESSearchRequest, - ESSearchBody, -} from '../../../../typings/elasticsearch'; +import { ESSearchBody } from '../../../../typings/elasticsearch'; import { Projection } from '../../typings'; +import { APMEventESSearchRequest } from '../../../lib/helpers/create_es_client/create_apm_event_client'; type PlainObject = Record; -type SourceProjection = Omit, 'body'> & { +type SourceProjection = Omit, 'body'> & { body: Omit, 'aggs'> & { aggs?: AggregationInputMap; }; diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index a47d72751dfc4..864f5033c9d62 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -13,23 +13,23 @@ import { SetupTimeRange, } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/ui_filters/get_environments'; -import { Projection } from '../../common/projections/typings'; +import { Projection } from '../projections/typings'; import { localUIFilterNames, LocalUIFilterName, } from '../lib/ui_filters/local_ui_filters/config'; import { getUiFiltersES } from '../lib/helpers/convert_ui_filters/get_ui_filters_es'; import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters'; -import { getServicesProjection } from '../../common/projections/services'; -import { getTransactionGroupsProjection } from '../../common/projections/transaction_groups'; -import { getMetricsProjection } from '../../common/projections/metrics'; -import { getErrorGroupsProjection } from '../../common/projections/errors'; -import { getTransactionsProjection } from '../../common/projections/transactions'; +import { getServicesProjection } from '../projections/services'; +import { getTransactionGroupsProjection } from '../projections/transaction_groups'; +import { getMetricsProjection } from '../projections/metrics'; +import { getErrorGroupsProjection } from '../projections/errors'; +import { getTransactionsProjection } from '../projections/transactions'; import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { jsonRt } from '../../common/runtime_types/json_rt'; -import { getServiceNodesProjection } from '../../common/projections/service_nodes'; -import { getRumOverviewProjection } from '../../common/projections/rum_overview'; +import { getServiceNodesProjection } from '../projections/service_nodes'; +import { getRumOverviewProjection } from '../projections/rum_overview'; export const uiFiltersEnvironmentsRoute = createRoute(() => ({ path: '/api/apm/ui_filters/environments', From 3793ae538148a1cdd650db9ecca2a141404875ee Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Fri, 31 Jul 2020 09:57:07 -0400 Subject: [PATCH 27/55] Check for security first (#73821) Co-authored-by: Elastic Machine --- .../__test__/get_collection_status.test.js | 52 ++++++++++++++++--- .../setup/collection/get_collection_status.js | 7 +++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js b/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js index e56627369475b..083ebfb27fd51 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js @@ -10,7 +10,12 @@ import { getCollectionStatus } from '..'; import { getIndexPatterns } from '../../../cluster/get_index_patterns'; const liveClusterUuid = 'a12'; -const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = true) => { +const mockReq = ( + searchResult = {}, + securityEnabled = true, + userHasPermissions = true, + securityErrorMessage = null +) => { return { server: { newPlatform: { @@ -37,12 +42,14 @@ const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = }, }, plugins: { - xpack_main: { + monitoring: { info: { - isAvailable: () => true, - feature: () => ({ - isEnabled: () => securityEnabled, - }), + getSecurityFeature: () => { + return { + isAvailable: securityEnabled, + isEnabled: securityEnabled, + }; + }, }, }, elasticsearch: { @@ -61,6 +68,11 @@ const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = params && params.path === '/_security/user/_has_privileges' ) { + if (securityErrorMessage !== null) { + return Promise.reject({ + message: securityErrorMessage, + }); + } return Promise.resolve({ has_all_requested: userHasPermissions }); } if (type === 'transport.request' && params && params.path === '/_nodes') { @@ -245,6 +257,34 @@ describe('getCollectionStatus', () => { expect(result.kibana.detected.doesExist).to.be(true); }); + it('should work properly with an unknown security message', async () => { + const req = mockReq({ hits: { total: { value: 1 } } }, true, true, 'foobar'); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result._meta.hasPermissions).to.be(false); + }); + + it('should work properly with a known security message', async () => { + const req = mockReq( + { hits: { total: { value: 1 } } }, + true, + true, + 'no handler found for uri [/_security/user/_has_privileges] and method [POST]' + ); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result.kibana.detected.doesExist).to.be(true); + }); + + it('should work properly with another known security message', async () => { + const req = mockReq( + { hits: { total: { value: 1 } } }, + true, + true, + 'Invalid index name [_security]' + ); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result.kibana.detected.doesExist).to.be(true); + }); + it('should not work if the user does not have the necessary permissions', async () => { const req = mockReq({ hits: { total: { value: 1 } } }, true, false); const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index 607503673276b..81cdfd6ecd172 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -233,6 +233,10 @@ function isBeatFromAPM(bucket) { } async function hasNecessaryPermissions(req) { + const securityFeature = req.server.plugins.monitoring.info.getSecurityFeature(); + if (!securityFeature.isAvailable || !securityFeature.isEnabled) { + return true; + } try { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data'); const response = await callWithRequest(req, 'transport.request', { @@ -250,6 +254,9 @@ async function hasNecessaryPermissions(req) { ) { return true; } + if (err.message.includes('Invalid index name [_security]')) { + return true; + } return false; } } From 9a1a6d35bc556a6debdd270f8cbf9ff720065b89 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Fri, 31 Jul 2020 10:04:58 -0400 Subject: [PATCH 28/55] Use defaultsDeep to match what monitoring is doing (#73325) Co-authored-by: Elastic Machine --- src/legacy/server/status/routes/api/register_stats.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js index 0221c7e0ea085..2cd780d21f681 100644 --- a/src/legacy/server/status/routes/api/register_stats.js +++ b/src/legacy/server/status/routes/api/register_stats.js @@ -19,6 +19,7 @@ import Joi from 'joi'; import boom from 'boom'; +import { defaultsDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { wrapAuthConfig } from '../../wrap_auth_config'; import { getKibanaInfoForStats } from '../../lib'; @@ -120,10 +121,9 @@ export function registerStatsApi(usageCollection, server, config, kbnServer) { }, }; } else { - accum = { - ...accum, - [usageKey]: usage[usageKey], - }; + // I don't think we need to it this for the above conditions, but do it for most as it will + // match the behavior done in monitoring/bulk_uploader + defaultsDeep(accum, { [usageKey]: usage[usageKey] }); } return accum; From 4454b30db2854296aa9903ac6152d453f29a82d8 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 31 Jul 2020 16:33:40 +0200 Subject: [PATCH 29/55] reset validation counter (#73459) --- .../vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts index a9b542af68c9d..dd748ea2d3815 100644 --- a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts +++ b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts @@ -42,5 +42,7 @@ export const tsvbTelemetrySavedObjectType: SavedObjectsType = { migrations: { '7.7.0': flow(resetCount), '7.8.0': flow(resetCount), + '7.9.0': flow(resetCount), + '7.10.0': flow(resetCount), }, }; From d51e277c3e41ed3aabe6a114682f74a534207757 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Fri, 31 Jul 2020 08:34:27 -0600 Subject: [PATCH 30/55] [Security Solution][Detections] Fixes risk score mapping bug and updates copy on empty rules message (#73901) ## Summary Fixes issue where Rules with a `Risk Score Mapping` could not be created. Fixes copy for the Rules Table empty view that says all rules are disabled by default (no longer true for the `Elastic Endpoint Security Rule`)

### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) --- .../components/rules/pre_packaged_rules/translations.ts | 2 +- .../detections/components/rules/risk_score_mapping/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts index 49da7dbf6d514..9b0cec99b1b38 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts @@ -17,7 +17,7 @@ export const PRE_BUILT_MSG = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage', { defaultMessage: - 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules are disabled and you select which rules you want to activate.', + 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules except the Elastic Endpoint Security rule are disabled. You can select additional rules you want to activate.', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index 35816e82540d1..0f16cb99862a5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -70,7 +70,7 @@ export const RiskScoreField = ({ { field: newField?.name ?? '', operator: 'equals', - value: undefined, + value: '', riskScore: undefined, }, ], From 747e9e47363581539d8a767ede9006c67f32479b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 31 Jul 2020 16:35:47 +0200 Subject: [PATCH 31/55] Stabilize graph test (#73918) --- x-pack/test/functional/apps/graph/graph.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index c2500dca78444..68e5045c1f36c 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -129,17 +129,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show venn when clicking a line', async function () { await buildGraph(); - const { edges } = await PageObjects.graph.getGraphObjects(); await PageObjects.graph.isolateEdge('test', '/test/wp-admin/'); await PageObjects.graph.stopLayout(); await PageObjects.common.sleep(1000); - const testTestWpAdminBlogEdge = edges.find( - ({ sourceNode, targetNode }) => - targetNode.label === '/test/wp-admin/' && sourceNode.label === 'test' - )!; - await testTestWpAdminBlogEdge.element.click(); + await browser.execute(() => { + const event = document.createEvent('SVGEvents'); + event.initEvent('click', true, true); + return document.getElementsByClassName('gphEdge')[0].dispatchEvent(event); + }); await PageObjects.common.sleep(1000); await PageObjects.graph.startLayout(); From df3e209262fbca24bdee9ed2353e077965a329b6 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 31 Jul 2020 10:39:01 -0400 Subject: [PATCH 32/55] [Uptime] Unskip alerting functional tests (#72963) * Unskip monitor status alert test. * Trying to resolve flakiness. * Remove commented code. * Simplify test expect. * Revert conditional block change. * Remove line in question. Co-authored-by: Elastic Machine --- .../functional/services/uptime/navigation.ts | 2 +- .../apps/uptime/alert_flyout.ts | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index ab511abf130a5..710923c886cbe 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -17,7 +17,7 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv if (await testSubjects.exists('uptimeSettingsToOverviewLink', { timeout: 0 })) { await testSubjects.click('uptimeSettingsToOverviewLink'); await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); - } else if (!(await testSubjects.exists('uptimeOverviewPage', { timeout: 0 }))) { + } else { await PageObjects.common.navigateToApp('uptime'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index 6cb74aff95be2..a6de87d6f7b1a 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -8,8 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - // FLAKY: https://github.com/elastic/kibana/issues/65948 - describe.skip('uptime alerts', () => { + describe('uptime alerts', () => { const pageObjects = getPageObjects(['common', 'uptime']); const supertest = getService('supertest'); const retry = getService('retry'); @@ -105,7 +104,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { alertTypeId, consumer, id, - params: { numTimes, timerange, locations, filters }, + params: { numTimes, timerangeUnit, timerangeCount, filters }, schedule: { interval }, tags, } = alert; @@ -119,14 +118,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(interval).to.eql('11m'); expect(tags).to.eql(['uptime', 'another']); expect(numTimes).to.be(3); - expect(timerange.from).to.be('now-1h'); - expect(timerange.to).to.be('now'); - expect(locations).to.eql(['mpls']); - expect(filters).to.eql( - '{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"mpls"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"url.port":5678}}],' + - '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}' + expect(timerangeUnit).to.be('h'); + expect(timerangeCount).to.be(1); + expect(JSON.stringify(filters)).to.eql( + `{"url.port":["5678"],"observer.geo.name":["mpls"],"monitor.type":["http"],"tags":[]}` ); } finally { await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); From ff3877d61db17aa8343357f29f50a409e256e16a Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Fri, 31 Jul 2020 09:43:56 -0500 Subject: [PATCH 33/55] Hide Canvas toolbar close button when tray is closed (#73845) Co-authored-by: Elastic Machine --- x-pack/plugins/canvas/public/components/toolbar/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/canvas/public/components/toolbar/index.js b/x-pack/plugins/canvas/public/components/toolbar/index.js index 16860063f8a45..a95371f5f032a 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/index.js +++ b/x-pack/plugins/canvas/public/components/toolbar/index.js @@ -44,6 +44,6 @@ export const Toolbar = compose( props.router.navigateTo('loadWorkpad', { id: props.workpadId, page: pageNumber }); }, }), - withState('tray', 'setTray', (props) => props.tray), + withState('tray', 'setTray', null), withState('showWorkpadManager', 'setShowWorkpadManager', false) )(Component); From 9c5bbe4e5f18df9e5f6212d24e7150b5f2cdcb84 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Fri, 31 Jul 2020 10:44:45 -0400 Subject: [PATCH 34/55] [ML] DF Analytics creation wizard: ensure user can switch back to form from JSON editor (#73752) * wip: add reducer action to switch to form * rename getFormStateFromJobConfig * wip: types fix * show destIndex input when switching back from editor * ensure validation up to date when switching to form * cannot switch back to form if advanced config * update types * localization fix --- .../details_step/details_step_form.tsx | 14 ++-- .../pages/analytics_creation/page.tsx | 71 +++++++++++-------- .../use_create_analytics_form/actions.ts | 5 +- .../use_create_analytics_form/reducer.ts | 57 +++++++++++++-- .../use_create_analytics_form/state.test.ts | 14 ++-- .../hooks/use_create_analytics_form/state.ts | 16 ++++- .../use_create_analytics_form.ts | 9 ++- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 9 files changed, 131 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 0ac237bb33e76..1d6a603caa817 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -44,7 +44,7 @@ export const DetailsStepForm: FC = ({ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const { setFormState } = actions; - const { form, cloneJob, isJobCreated } = state; + const { form, cloneJob, hasSwitchedToEditor, isJobCreated } = state; const { createIndexPattern, description, @@ -61,7 +61,9 @@ export const DetailsStepForm: FC = ({ resultsField, } = form; - const [destIndexSameAsId, setDestIndexSameAsId] = useState(cloneJob === undefined); + const [destIndexSameAsId, setDestIndexSameAsId] = useState( + cloneJob === undefined && hasSwitchedToEditor === false + ); const forceInput = useRef(null); @@ -90,7 +92,11 @@ export const DetailsStepForm: FC = ({ useEffect(() => { if (destinationIndexNameValid === true) { debouncedIndexCheck(); - } else if (destinationIndex.trim() === '' && destinationIndexNameExists === true) { + } else if ( + typeof destinationIndex === 'string' && + destinationIndex.trim() === '' && + destinationIndexNameExists === true + ) { setFormState({ destinationIndexNameExists: false }); } @@ -102,7 +108,7 @@ export const DetailsStepForm: FC = ({ useEffect(() => { if (destIndexSameAsId === true && !jobIdEmpty && jobIdValid) { setFormState({ destinationIndex: jobId }); - } else if (destIndexSameAsId === false) { + } else if (destIndexSameAsId === false && hasSwitchedToEditor === false) { setFormState({ destinationIndex: '' }); } }, [destIndexSameAsId, jobId]); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index 04dd25896d443..2f0e2ed3428c0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -6,7 +6,6 @@ import React, { FC, useEffect, useState } from 'react'; import { - EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -16,7 +15,7 @@ import { EuiSpacer, EuiSteps, EuiStepStatus, - EuiText, + EuiSwitch, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -48,9 +47,15 @@ export const Page: FC = ({ jobId }) => { const { currentIndexPattern } = mlContext; const createAnalyticsForm = useCreateAnalyticsForm(); - const { isAdvancedEditorEnabled } = createAnalyticsForm.state; - const { jobType } = createAnalyticsForm.state.form; - const { initiateWizard, setJobClone, switchToAdvancedEditor } = createAnalyticsForm.actions; + const { state } = createAnalyticsForm; + const { isAdvancedEditorEnabled, disableSwitchToForm } = state; + const { jobType } = state.form; + const { + initiateWizard, + setJobClone, + switchToAdvancedEditor, + switchToForm, + } = createAnalyticsForm.actions; useEffect(() => { initiateWizard(); @@ -170,34 +175,40 @@ export const Page: FC = ({ jobId }) => { - {isAdvancedEditorEnabled === false && ( - - + + - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.create.switchToJsonEditorSwitch', - { - defaultMessage: 'Switch to json editor', - } - )} - - - - - )} + checked={isAdvancedEditorEnabled} + onChange={(e) => { + if (e.target.checked === true) { + switchToAdvancedEditor(); + } else { + switchToForm(); + } + }} + data-test-subj="mlAnalyticsCreateJobWizardAdvancedEditorSwitch" + /> + + {isAdvancedEditorEnabled === true && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts index 4bfee9f308313..5f3045696f170 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts @@ -25,6 +25,7 @@ export enum ACTION { SET_JOB_CONFIG, SET_JOB_IDS, SWITCH_TO_ADVANCED_EDITOR, + SWITCH_TO_FORM, SET_ESTIMATED_MODEL_MEMORY_LIMIT, SET_JOB_CLONE, } @@ -38,7 +39,8 @@ export type Action = | ACTION.OPEN_MODAL | ACTION.RESET_ADVANCED_EDITOR_MESSAGES | ACTION.RESET_FORM - | ACTION.SWITCH_TO_ADVANCED_EDITOR; + | ACTION.SWITCH_TO_ADVANCED_EDITOR + | ACTION.SWITCH_TO_FORM; } // Actions with custom payloads: | { type: ACTION.ADD_REQUEST_MESSAGE; requestMessage: FormMessage } @@ -71,6 +73,7 @@ export interface ActionDispatchers { setJobConfig: (payload: State['jobConfig']) => void; startAnalyticsJob: () => void; switchToAdvancedEditor: () => void; + switchToForm: () => void; setEstimatedModelMemoryLimit: (value: State['estimatedModelMemoryLimit']) => void; setJobClone: (cloneJob: DeepReadonly) => Promise; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index acdaf15cdf4b7..8d8421a116b91 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -8,13 +8,17 @@ import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; // @ts-ignore import numeral from '@elastic/numeral'; -import { isEmpty } from 'lodash'; import { isValidIndexName } from '../../../../../../../common/util/es_utils'; import { collapseLiteralStrings } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; import { Action, ACTION } from './actions'; -import { getInitialState, getJobConfigFromFormState, State } from './state'; +import { + getInitialState, + getFormStateFromJobConfig, + getJobConfigFromFormState, + State, +} from './state'; import { isJobIdValid, validateModelMemoryLimitUnits, @@ -41,6 +45,7 @@ import { TRAINING_PERCENT_MAX, } from '../../../../common/analytics'; import { indexPatterns } from '../../../../../../../../../../src/plugins/data/public'; +import { isAdvancedConfig } from '../../components/action_clone/clone_button'; const mmlAllowedUnitsStr = `${ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join( ', ' @@ -458,13 +463,16 @@ export function reducer(state: State, action: Action): State { case ACTION.SET_ADVANCED_EDITOR_RAW_STRING: let resultJobConfig; + let disableSwitchToForm = false; try { resultJobConfig = JSON.parse(collapseLiteralStrings(action.advancedEditorRawString)); + disableSwitchToForm = isAdvancedConfig(resultJobConfig); } catch (e) { return { ...state, advancedEditorRawString: action.advancedEditorRawString, isAdvancedEditorValidJson: false, + disableSwitchToForm: true, advancedEditorMessages: [], }; } @@ -473,6 +481,7 @@ export function reducer(state: State, action: Action): State { ...validateAdvancedEditor({ ...state, jobConfig: resultJobConfig }), advancedEditorRawString: action.advancedEditorRawString, isAdvancedEditorValidJson: true, + disableSwitchToForm, }; case ACTION.SET_FORM_STATE: @@ -538,17 +547,53 @@ export function reducer(state: State, action: Action): State { case ACTION.SWITCH_TO_ADVANCED_EDITOR: let { jobConfig } = state; - const isJobConfigEmpty = isEmpty(state.jobConfig); - if (isJobConfigEmpty) { - jobConfig = getJobConfigFromFormState(state.form); - } + jobConfig = getJobConfigFromFormState(state.form); + const shouldDisableSwitchToForm = isAdvancedConfig(jobConfig); + return validateAdvancedEditor({ ...state, advancedEditorRawString: JSON.stringify(jobConfig, null, 2), isAdvancedEditorEnabled: true, + disableSwitchToForm: shouldDisableSwitchToForm, + hasSwitchedToEditor: true, jobConfig, }); + case ACTION.SWITCH_TO_FORM: + const { jobConfig: config, jobIds } = state; + const { jobId } = state.form; + // @ts-ignore + const formState = getFormStateFromJobConfig(config, false); + + if (typeof jobId === 'string' && jobId.trim() !== '') { + formState.jobId = jobId; + } + + formState.jobIdExists = jobIds.some((id) => formState.jobId === id); + formState.jobIdEmpty = jobId === ''; + formState.jobIdValid = isJobIdValid(jobId); + formState.jobIdInvalidMaxLength = !!maxLengthValidator(JOB_ID_MAX_LENGTH)(jobId); + + formState.destinationIndexNameEmpty = formState.destinationIndex === ''; + formState.destinationIndexNameValid = isValidIndexName(formState.destinationIndex || ''); + formState.destinationIndexPatternTitleExists = + state.indexPatternsMap[formState.destinationIndex || ''] !== undefined; + + if (formState.numTopFeatureImportanceValues !== undefined) { + formState.numTopFeatureImportanceValuesValid = validateNumTopFeatureImportanceValues( + formState.numTopFeatureImportanceValues + ); + } + + return validateForm({ + ...state, + // @ts-ignore + form: formState, + isAdvancedEditorEnabled: false, + advancedEditorRawString: JSON.stringify(config, null, 2), + jobConfig: config, + }); + case ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT: return { ...state, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts index d397dfc315da4..499318ebddc19 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getCloneFormStateFromJobConfig, - getInitialState, - getJobConfigFromFormState, -} from './state'; +import { getFormStateFromJobConfig, getInitialState, getJobConfigFromFormState } from './state'; const regJobConfig = { id: 'reg-test-01', @@ -96,8 +92,8 @@ describe('useCreateAnalyticsForm', () => { ]); }); - test('state: getCloneFormStateFromJobConfig() regression', () => { - const clonedState = getCloneFormStateFromJobConfig(regJobConfig); + test('state: getFormStateFromJobConfig() regression', () => { + const clonedState = getFormStateFromJobConfig(regJobConfig); expect(clonedState?.sourceIndex).toBe('reg-test-index'); expect(clonedState?.includes).toStrictEqual([]); @@ -112,8 +108,8 @@ describe('useCreateAnalyticsForm', () => { expect(clonedState?.jobId).toBe(undefined); }); - test('state: getCloneFormStateFromJobConfig() outlier detection', () => { - const clonedState = getCloneFormStateFromJobConfig(outlierJobConfig); + test('state: getFormStateFromJobConfig() outlier detection', () => { + const clonedState = getFormStateFromJobConfig(outlierJobConfig); expect(clonedState?.sourceIndex).toBe('outlier-test-index'); expect(clonedState?.includes).toStrictEqual(['field', 'other_field']); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 725fc8751408e..69599f43ef297 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -12,6 +12,7 @@ import { DataFrameAnalyticsId, DataFrameAnalyticsConfig, ANALYSIS_CONFIG_TYPE, + defaultSearchQuery, } from '../../../../common/analytics'; import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone'; @@ -44,6 +45,7 @@ export interface FormMessage { export interface State { advancedEditorMessages: FormMessage[]; advancedEditorRawString: string; + disableSwitchToForm: boolean; form: { computeFeatureInfluence: string; createIndexPattern: boolean; @@ -97,6 +99,7 @@ export interface State { indexPatternsMap: SourceIndexMap; isAdvancedEditorEnabled: boolean; isAdvancedEditorValidJson: boolean; + hasSwitchedToEditor: boolean; isJobCreated: boolean; isJobStarted: boolean; isValid: boolean; @@ -110,6 +113,7 @@ export interface State { export const getInitialState = (): State => ({ advancedEditorMessages: [], advancedEditorRawString: '', + disableSwitchToForm: false, form: { computeFeatureInfluence: 'true', createIndexPattern: true, @@ -131,7 +135,7 @@ export const getInitialState = (): State => ({ jobIdInvalidMaxLength: false, jobIdValid: false, jobType: undefined, - jobConfigQuery: { match_all: {} }, + jobConfigQuery: defaultSearchQuery, jobConfigQueryString: undefined, lambda: undefined, loadingFieldOptions: false, @@ -167,6 +171,7 @@ export const getInitialState = (): State => ({ indexPatternsMap: {}, isAdvancedEditorEnabled: false, isAdvancedEditorValidJson: true, + hasSwitchedToEditor: false, isJobCreated: false, isJobStarted: false, isValid: false, @@ -283,8 +288,9 @@ function toCamelCase(property: string): string { * Extracts form state for a job clone from the analytics job configuration. * For cloning we keep job id and destination index empty. */ -export function getCloneFormStateFromJobConfig( - analyticsJobConfig: Readonly +export function getFormStateFromJobConfig( + analyticsJobConfig: Readonly, + isClone: boolean = true ): Partial { const jobType = Object.keys(analyticsJobConfig.analysis)[0] as ANALYSIS_CONFIG_TYPE; @@ -300,6 +306,10 @@ export function getCloneFormStateFromJobConfig( includes: analyticsJobConfig.analyzed_fields.includes, }; + if (isClone === false) { + resultState.destinationIndex = analyticsJobConfig?.dest.index ?? ''; + } + const analysisConfig = analyticsJobConfig.analysis[jobType]; for (const key in analysisConfig) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 035610684d556..9612b9213d120 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -28,7 +28,7 @@ import { FormMessage, State, SourceIndexMap, - getCloneFormStateFromJobConfig, + getFormStateFromJobConfig, } from './state'; import { ANALYTICS_STEPS } from '../../../analytics_creation/page'; @@ -283,6 +283,10 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { dispatch({ type: ACTION.SWITCH_TO_ADVANCED_EDITOR }); }; + const switchToForm = () => { + dispatch({ type: ACTION.SWITCH_TO_FORM }); + }; + const setEstimatedModelMemoryLimit = (value: State['estimatedModelMemoryLimit']) => { dispatch({ type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT, value }); }; @@ -294,7 +298,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { setJobConfig(config); switchToAdvancedEditor(); } else { - setFormState(getCloneFormStateFromJobConfig(config)); + setFormState(getFormStateFromJobConfig(config)); setEstimatedModelMemoryLimit(config.model_memory_limit); } @@ -311,6 +315,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { setJobConfig, startAnalyticsJob, switchToAdvancedEditor, + switchToForm, setEstimatedModelMemoryLimit, setJobClone, }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c81aade2b063e..25d37334d0b82 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11084,7 +11084,6 @@ "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "編集", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "Kibanaインデックスパターンの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "インデックスパターン{indexPatternName}はすでに作成されています。", - "xpack.ml.dataframe.analytics.create.enableJsonEditorHelpText": "JSONエディターからこのフォームには戻れません。", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "データフレーム分析ジョブの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "既存のデータフレーム分析ジョブIDの取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index aba5adf72c2f8..b3886ffb1ecb1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11086,7 +11086,6 @@ "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "编辑", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "创建 Kibana 索引模式时发生错误:", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "索引模式 {indexPatternName} 已存在。", - "xpack.ml.dataframe.analytics.create.enableJsonEditorHelpText": "您不能从 json 编辑器切回到此表单。", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "创建数据帧分析作业时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "获取现有数据帧分析作业 ID 时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", From 147d4c7ad0afe83143faa22136aa56a821ef57e6 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 31 Jul 2020 08:52:27 -0600 Subject: [PATCH 35/55] [SIEM] Fixes a bug where invalid regular expressions within the index patterns can cause UI toaster errors (#73754) ## Summary https://github.com/elastic/kibana/issues/49753 When you have no data you get a toaster error when we don't want a toaster error. Before with the toaster error: ![error](https://user-images.githubusercontent.com/1151048/88860918-0e2a5900-d1ba-11ea-95e7-5ed7324fc831.png) After: You don't get an error toaster because I catch any regular expression errors and do not report them up to the UI. ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../server/utils/beat_schema/index.test.ts | 7 +++++++ .../server/utils/beat_schema/index.ts | 14 ++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts index 56ceca2b70e9c..5f002aa7fad7b 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts @@ -401,10 +401,17 @@ describe('Schema Beat', () => { const result = getIndexAlias([leadingWildcardIndex], leadingWildcardIndex); expect(result).toBe(leadingWildcardIndex); }); + test('getIndexAlias no match returns "unknown" string', () => { const index = 'auditbeat-*'; const result = getIndexAlias([index], 'hello'); expect(result).toBe('unknown'); }); + + test('empty index should not cause an error to return although it will cause an invalid regular expression to occur', () => { + const index = ''; + const result = getIndexAlias([index], 'hello'); + expect(result).toBe('unknown'); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts index ff7331cf39bc7..6ec15d328714d 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts @@ -77,10 +77,16 @@ const convertFieldsToAssociativeArray = ( : {}; export const getIndexAlias = (defaultIndex: string[], indexName: string): string => { - const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null); - if (found != null) { - return found; - } else { + try { + const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null); + if (found != null) { + return found; + } else { + return 'unknown'; + } + } catch (error) { + // if we encounter an error because the index contains invalid regular expressions then we should return an unknown + // rather than blow up with a toaster error upstream return 'unknown'; } }; From 61194b65aaa2f04bc0600171eeecc2bc56985640 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 31 Jul 2020 11:04:41 -0400 Subject: [PATCH 36/55] [Uptime] Unskip certs func tests (#73086) * Temporarily unload all other functional tests. * Unskip certificates test. * Uncomment skipped test files. * Add explicit fields to check generator call to prevent grouping issues that can lead to test flakiness. * added wait for loading * update missing func Co-authored-by: Elastic Machine Co-authored-by: Shahzad --- x-pack/test/functional/apps/uptime/certificates.ts | 12 ++++++++++-- x-pack/test/functional/apps/uptime/index.ts | 1 + .../test/functional/services/uptime/certificates.ts | 5 ++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index ccf35a1e63e37..7e9a2cd85935e 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -14,8 +14,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); - // Failing: See https://github.com/elastic/kibana/issues/70493 - describe.skip('certificates', function () { + describe('certificates', function () { before(async () => { await makeCheck({ es, tls: true }); await uptime.goToRoot(true); @@ -58,6 +57,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const certId = getSha256(); const { monitorId } = await makeCheck({ es, + monitorId: 'cert-test-check-id', + fields: { + monitor: { + name: 'Cert Test Check', + }, + url: { + full: 'https://site-to-check.com/', + }, + }, tls: { sha256: certId, }, diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index 028ab3ff8803a..6b2b61cba2b64 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -55,6 +55,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./settings')); loadTestFile(require.resolve('./certificates')); }); + describe('with real-world data', () => { before(async () => { await esArchiver.unload(ARCHIVE); diff --git a/x-pack/test/functional/services/uptime/certificates.ts b/x-pack/test/functional/services/uptime/certificates.ts index 2ceab1ca89e54..06de9be5af7e9 100644 --- a/x-pack/test/functional/services/uptime/certificates.ts +++ b/x-pack/test/functional/services/uptime/certificates.ts @@ -7,10 +7,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export function UptimeCertProvider({ getService }: FtrProviderContext) { +export function UptimeCertProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'timePicker', 'header']); + const changeSearchField = async (text: string) => { const input = await testSubjects.find('uptimeCertSearch'); await input.clearValueWithKeyboard(); @@ -61,6 +63,7 @@ export function UptimeCertProvider({ getService }: FtrProviderContext) { const self = this; return retry.tryForTime(60 * 1000, async () => { await changeSearchField(monId); + await PageObjects.header.waitUntilLoadingHasFinished(); await self.hasCertificates(1); }); }, From a1872b1a7029d871bd72cf29e5edac7cadc3e25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Fri, 31 Jul 2020 17:11:01 +0200 Subject: [PATCH 37/55] [ML] Removes link from helper text on ML overview page (#73819) --- .../overview/components/sidebar.tsx | 20 +------------------ .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx index 119346ec8035a..903a3c467a38b 100644 --- a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx @@ -9,29 +9,12 @@ import { EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui import { FormattedMessage } from '@kbn/i18n/react'; import { useMlKibana } from '../../contexts/kibana'; -const createJobLink = '#/jobs/new_job/step/index_or_search'; const feedbackLink = 'https://www.elastic.co/community/'; interface Props { createAnomalyDetectionJobDisabled: boolean; } -function getCreateJobLink(createAnomalyDetectionJobDisabled: boolean) { - return createAnomalyDetectionJobDisabled === true ? ( - - ) : ( - - - - ); -} - export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }) => { const { services: { @@ -59,7 +42,7 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }

@@ -69,7 +52,6 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled } /> ), - createJob: getCreateJobLink(createAnomalyDetectionJobDisabled), transforms: ( Date: Fri, 31 Jul 2020 11:14:00 -0400 Subject: [PATCH 38/55] moved config option for allowing or disallowing by value embeddables to dashboard plugin (#73870) --- src/plugins/dashboard/config.ts | 26 +++++++++++++++++++++++++ src/plugins/dashboard/public/plugin.tsx | 6 ++++++ src/plugins/dashboard/server/index.ts | 10 +++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/plugins/dashboard/config.ts diff --git a/src/plugins/dashboard/config.ts b/src/plugins/dashboard/config.ts new file mode 100644 index 0000000000000..ff968a51679e0 --- /dev/null +++ b/src/plugins/dashboard/config.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + allowByValueEmbeddables: schema.boolean({ defaultValue: false }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index f0b57fec169fd..f1319665d258b 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -94,6 +94,10 @@ declare module '../../share/public' { export type DashboardUrlGenerator = UrlGeneratorContract; +interface DashboardFeatureFlagConfig { + allowByValueEmbeddables: boolean; +} + interface SetupDependencies { data: DataPublicPluginSetup; embeddable: EmbeddableSetup; @@ -125,6 +129,7 @@ export interface DashboardStart { embeddableType: string; }) => void | undefined; dashboardUrlGenerator?: DashboardUrlGenerator; + dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; DashboardContainerByValueRenderer: ReturnType; } @@ -411,6 +416,7 @@ export class DashboardPlugin getSavedDashboardLoader: () => savedDashboardLoader, addEmbeddableToDashboard: this.addEmbeddableToDashboard.bind(this, core), dashboardUrlGenerator: this.dashboardUrlGenerator, + dashboardFeatureFlagConfig: this.initializerContext.config.get(), DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), diff --git a/src/plugins/dashboard/server/index.ts b/src/plugins/dashboard/server/index.ts index 9719586001c59..3ef7abba5776b 100644 --- a/src/plugins/dashboard/server/index.ts +++ b/src/plugins/dashboard/server/index.ts @@ -17,8 +17,16 @@ * under the License. */ -import { PluginInitializerContext } from '../../../core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from '../../../core/server'; import { DashboardPlugin } from './plugin'; +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + allowByValueEmbeddables: true, + }, + schema: configSchema, +}; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. From 10cc600d5cdc9239ce174683b9fee6c3b22c8f4c Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Fri, 31 Jul 2020 09:19:43 -0600 Subject: [PATCH 39/55] Fix aborted$ event and add completed$ event to KibanaRequest (#73898) --- ...e-server.kibanarequestevents.completed_.md | 18 ++++ ...-plugin-core-server.kibanarequestevents.md | 1 + .../http/integration_tests/request.test.ts | 91 +++++++++++++++++++ src/core/server/http/router/request.ts | 19 +++- src/core/server/server.api.md | 1 + 5 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md new file mode 100644 index 0000000000000..c9f8ab11f6b12 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) > [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md) + +## KibanaRequestEvents.completed$ property + +Observable that emits once if and when the request has been completely handled. + +Signature: + +```typescript +completed$: Observable; +``` + +## Remarks + +The request may be considered completed if: - A response has been sent to the client; or - The request was aborted. + diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md index 21826c8b29383..dfd7efd27cb5a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md @@ -17,4 +17,5 @@ export interface KibanaRequestEvents | Property | Type | Description | | --- | --- | --- | | [aborted$](./kibana-plugin-core-server.kibanarequestevents.aborted_.md) | Observable<void> | Observable that emits once if and when the request has been aborted. | +| [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md) | Observable<void> | Observable that emits once if and when the request has been completely handled. | diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index 2d018f7f464b5..3a7335583296e 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -23,6 +23,7 @@ import { HttpService } from '../http_service'; import { contextServiceMock } from '../../context/context_service.mock'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { createHttpServer } from '../test_utils'; +import { schema } from '@kbn/config-schema'; let server: HttpService; @@ -195,6 +196,96 @@ describe('KibanaRequest', () => { expect(nextSpy).toHaveBeenCalledTimes(0); expect(completeSpy).toHaveBeenCalledTimes(1); }); + + it('does not complete before response has been sent', async () => { + const { server: innerServer, createRouter, registerOnPreAuth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + const completeSpy = jest.fn(); + + registerOnPreAuth((req, res, toolkit) => { + req.events.aborted$.subscribe({ + next: nextSpy, + complete: completeSpy, + }); + return toolkit.next(); + }); + + router.post( + { path: '/', validate: { body: schema.any() } }, + async (context, request, res) => { + expect(completeSpy).not.toHaveBeenCalled(); + return res.ok({ body: 'ok' }); + } + ); + + await server.start(); + + await supertest(innerServer.listener).post('/').send({ data: 'test' }).expect(200); + + expect(nextSpy).toHaveBeenCalledTimes(0); + expect(completeSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('completed$', () => { + it('emits once and completes when response is sent', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + const completeSpy = jest.fn(); + + router.get({ path: '/', validate: false }, async (context, req, res) => { + req.events.completed$.subscribe({ + next: nextSpy, + complete: completeSpy, + }); + + expect(nextSpy).not.toHaveBeenCalled(); + expect(completeSpy).not.toHaveBeenCalled(); + return res.ok({ body: 'ok' }); + }); + + await server.start(); + + await supertest(innerServer.listener).get('/').expect(200); + expect(nextSpy).toHaveBeenCalledTimes(1); + expect(completeSpy).toHaveBeenCalledTimes(1); + }); + + it('emits once and completes when response is aborted', async (done) => { + expect.assertions(2); + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + + router.get({ path: '/', validate: false }, async (context, req, res) => { + req.events.completed$.subscribe({ + next: nextSpy, + complete: () => { + expect(nextSpy).toHaveBeenCalledTimes(1); + done(); + }, + }); + + expect(nextSpy).not.toHaveBeenCalled(); + await delay(30000); + return res.ok({ body: 'ok' }); + }); + + await server.start(); + + const incomingRequest = supertest(innerServer.listener) + .get('/') + // end required to send request + .end(); + setTimeout(() => incomingRequest.abort(), 50); + }); }); }); }); diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 0e73431fe7c6d..93ffb5aa48259 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -64,6 +64,16 @@ export interface KibanaRequestEvents { * Observable that emits once if and when the request has been aborted. */ aborted$: Observable; + + /** + * Observable that emits once if and when the request has been completely handled. + * + * @remarks + * The request may be considered completed if: + * - A response has been sent to the client; or + * - The request was aborted. + */ + completed$: Observable; } /** @@ -186,11 +196,16 @@ export class KibanaRequest< private getEvents(request: Request): KibanaRequestEvents { const finish$ = merge( - fromEvent(request.raw.req, 'end'), // all data consumed + fromEvent(request.raw.res, 'finish'), // Response has been sent fromEvent(request.raw.req, 'close') // connection was closed ).pipe(shareReplay(1), first()); + + const aborted$ = fromEvent(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)); + const completed$ = merge(finish$, aborted$).pipe(shareReplay(1), first()); + return { - aborted$: fromEvent(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)), + aborted$, + completed$, } as const; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c1054c27d084e..21ef66230f698 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1071,6 +1071,7 @@ export class KibanaRequest; + completed$: Observable; } // @public From 708a30abb329355a9df7fab2f99938a2fd0c6654 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Fri, 31 Jul 2020 11:39:12 -0400 Subject: [PATCH 40/55] [Canvas][tech-debt] Update Redux components to reflect new structure (#73844) Co-authored-by: Elastic Machine --- .../embeddable_flyout/flyout.component.tsx | 78 ++++++ .../components/embeddable_flyout/flyout.tsx | 160 +++++++----- .../components/embeddable_flyout/index.ts | 11 + .../components/embeddable_flyout/index.tsx | 113 -------- .../public/components/page_manager/index.ts | 27 +- ...manager.tsx => page_manager.component.tsx} | 0 .../components/page_manager/page_manager.ts | 31 +++ .../public/components/page_preview/index.ts | 20 +- ...preview.tsx => page_preview.component.tsx} | 0 .../components/page_preview/page_preview.ts | 24 ++ .../saved_elements_modal.stories.tsx | 2 +- .../components/saved_elements_modal/index.ts | 132 +--------- ...tsx => saved_elements_modal.component.tsx} | 0 .../saved_elements_modal.ts | 136 ++++++++++ .../element_settings.component.tsx | 55 ++++ .../element_settings/element_settings.tsx | 63 ++--- .../sidebar/element_settings/index.ts | 8 + .../sidebar/element_settings/index.tsx | 34 --- .../components/workpad_color_picker/index.ts | 19 +- ...tsx => workpad_color_picker.component.tsx} | 0 .../workpad_color_picker.ts | 23 ++ .../public/components/workpad_config/index.ts | 39 +-- ...onfig.tsx => workpad_config.component.tsx} | 0 .../workpad_config/workpad_config.ts | 43 ++++ .../__examples__/edit_menu.stories.tsx | 2 +- ...{edit_menu.tsx => edit_menu.component.tsx} | 0 .../workpad_header/edit_menu/edit_menu.ts | 133 ++++++++++ .../workpad_header/edit_menu/index.ts | 129 +--------- .../__examples__/element_menu.stories.tsx | 2 +- .../element_menu/element_menu.component.tsx | 214 ++++++++++++++++ .../element_menu/element_menu.tsx | 241 +++--------------- .../workpad_header/element_menu/index.ts | 8 + .../workpad_header/element_menu/index.tsx | 47 ---- .../public/components/workpad_header/index.ts | 8 + .../components/workpad_header/index.tsx | 46 ---- .../workpad_header/refresh_control/index.ts | 18 +- ...trol.tsx => refresh_control.component.tsx} | 0 .../refresh_control/refresh_control.ts | 22 ++ .../__examples__/share_menu.stories.tsx | 2 +- ..._flyout.stories.tsx => flyout.stories.tsx} | 2 +- ...ebsite_flyout.tsx => flyout.component.tsx} | 2 +- .../share_menu/flyout/flyout.ts | 101 ++++++++ .../workpad_header/share_menu/flyout/index.ts | 94 +------ .../share_menu/flyout/runtime_step.tsx | 2 +- .../share_menu/flyout/snippets_step.tsx | 2 +- .../share_menu/flyout/workpad_step.tsx | 2 +- .../workpad_header/share_menu/index.ts | 94 +------ ...hare_menu.tsx => share_menu.component.tsx} | 0 .../workpad_header/share_menu/share_menu.ts | 98 +++++++ .../__examples__/view_menu.stories.tsx | 2 +- .../workpad_header/view_menu/index.ts | 96 +------ ...{view_menu.tsx => view_menu.component.tsx} | 0 .../workpad_header/view_menu/view_menu.ts | 100 ++++++++ .../workpad_header.component.tsx | 150 +++++++++++ .../workpad_header/workpad_header.tsx | 174 +++---------- .../canvas/storybook/storyshots.test.js | 2 +- 56 files changed, 1466 insertions(+), 1345 deletions(-) create mode 100644 x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts delete mode 100644 x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx rename x-pack/plugins/canvas/public/components/page_manager/{page_manager.tsx => page_manager.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/page_manager/page_manager.ts rename x-pack/plugins/canvas/public/components/page_preview/{page_preview.tsx => page_preview.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/page_preview/page_preview.ts rename x-pack/plugins/canvas/public/components/saved_elements_modal/{saved_elements_modal.tsx => saved_elements_modal.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts create mode 100644 x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts delete mode 100644 x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx rename x-pack/plugins/canvas/public/components/workpad_color_picker/{workpad_color_picker.tsx => workpad_color_picker.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts rename x-pack/plugins/canvas/public/components/workpad_config/{workpad_config.tsx => workpad_config.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts rename x-pack/plugins/canvas/public/components/workpad_header/edit_menu/{edit_menu.tsx => edit_menu.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts delete mode 100644 x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/index.ts delete mode 100644 x-pack/plugins/canvas/public/components/workpad_header/index.tsx rename x-pack/plugins/canvas/public/components/workpad_header/refresh_control/{refresh_control.tsx => refresh_control.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/{share_website_flyout.stories.tsx => flyout.stories.tsx} (94%) rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/{share_website_flyout.tsx => flyout.component.tsx} (98%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.ts rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/{share_menu.tsx => share_menu.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts rename x-pack/plugins/canvas/public/components/workpad_header/view_menu/{view_menu.tsx => view_menu.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx new file mode 100644 index 0000000000000..0b5bd8adf8cb9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; +import { + SavedObjectFinderUi, + SavedObjectMetaData, +} from '../../../../../../src/plugins/saved_objects/public/'; +import { ComponentStrings } from '../../../i18n'; +import { useServices } from '../../services'; + +const { AddEmbeddableFlyout: strings } = ComponentStrings; + +export interface Props { + onClose: () => void; + onSelect: (id: string, embeddableType: string) => void; + availableEmbeddables: string[]; +} + +export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { + const services = useServices(); + const { embeddables, platform } = services; + const { getEmbeddableFactories } = embeddables; + const { getSavedObjects, getUISettings } = platform; + + const onAddPanel = (id: string, savedObjectType: string, name: string) => { + const embeddableFactories = getEmbeddableFactories(); + + // Find the embeddable type from the saved object type + const found = Array.from(embeddableFactories).find((embeddableFactory) => { + return Boolean( + embeddableFactory.savedObjectMetaData && + embeddableFactory.savedObjectMetaData.type === savedObjectType + ); + }); + + const foundEmbeddableType = found ? found.type : 'unknown'; + + onSelect(id, foundEmbeddableType); + }; + + const embeddableFactories = getEmbeddableFactories(); + + const availableSavedObjects = Array.from(embeddableFactories) + .filter((factory) => { + return availableEmbeddables.includes(factory.type); + }) + .map((factory) => factory.savedObjectMetaData) + .filter>(function ( + maybeSavedObjectMetaData + ): maybeSavedObjectMetaData is SavedObjectMetaData<{}> { + return maybeSavedObjectMetaData !== undefined; + }); + + return ( + + + +

{strings.getTitleText()}

+ + + + + + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index 0b5bd8adf8cb9..8c84e3d7a85d8 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -4,75 +4,107 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; -import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; -import { - SavedObjectFinderUi, - SavedObjectMetaData, -} from '../../../../../../src/plugins/saved_objects/public/'; -import { ComponentStrings } from '../../../i18n'; -import { useServices } from '../../services'; - -const { AddEmbeddableFlyout: strings } = ComponentStrings; - -export interface Props { - onClose: () => void; - onSelect: (id: string, embeddableType: string) => void; - availableEmbeddables: string[]; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { compose } from 'recompose'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { AddEmbeddableFlyout as Component, Props as ComponentProps } from './flyout.component'; +// @ts-expect-error untyped local +import { addElement } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; + +const allowedEmbeddables = { + [EmbeddableTypes.map]: (id: string) => { + return `savedMap id="${id}" | render`; + }, + [EmbeddableTypes.lens]: (id: string) => { + return `savedLens id="${id}" | render`; + }, + [EmbeddableTypes.visualization]: (id: string) => { + return `savedVisualization id="${id}" | render`; + }, + /* + [EmbeddableTypes.search]: (id: string) => { + return `filters | savedSearch id="${id}" | render`; + },*/ +}; + +interface StateProps { + pageId: string; +} + +interface DispatchProps { + addEmbeddable: (pageId: string, partialElement: { expression: string }) => void; } -export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { - const services = useServices(); - const { embeddables, platform } = services; - const { getEmbeddableFactories } = embeddables; - const { getSavedObjects, getUISettings } = platform; +// FIX: Missing state type +const mapStateToProps = (state: any) => ({ pageId: getSelectedPage(state) }); - const onAddPanel = (id: string, savedObjectType: string, name: string) => { - const embeddableFactories = getEmbeddableFactories(); +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + addEmbeddable: (pageId, partialElement): DispatchProps['addEmbeddable'] => + dispatch(addElement(pageId, partialElement)), +}); - // Find the embeddable type from the saved object type - const found = Array.from(embeddableFactories).find((embeddableFactory) => { - return Boolean( - embeddableFactory.savedObjectMetaData && - embeddableFactory.savedObjectMetaData.type === savedObjectType - ); - }); +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => { + const { pageId, ...remainingStateProps } = stateProps; + const { addEmbeddable } = dispatchProps; - const foundEmbeddableType = found ? found.type : 'unknown'; + return { + ...remainingStateProps, + ...ownProps, + onSelect: (id: string, type: string): void => { + const partialElement = { + expression: `markdown "Could not find embeddable for type ${type}" | render`, + }; + if (allowedEmbeddables[type]) { + partialElement.expression = allowedEmbeddables[type](id); + } - onSelect(id, foundEmbeddableType); + addEmbeddable(pageId, partialElement); + ownProps.onClose(); + }, }; - - const embeddableFactories = getEmbeddableFactories(); - - const availableSavedObjects = Array.from(embeddableFactories) - .filter((factory) => { - return availableEmbeddables.includes(factory.type); - }) - .map((factory) => factory.savedObjectMetaData) - .filter>(function ( - maybeSavedObjectMetaData - ): maybeSavedObjectMetaData is SavedObjectMetaData<{}> { - return maybeSavedObjectMetaData !== undefined; - }); - - return ( - - - -

{strings.getTitleText()}

-
-
- - - -
- ); }; + +export class EmbeddableFlyoutPortal extends React.Component { + el?: HTMLElement; + + constructor(props: ComponentProps) { + super(props); + + this.el = document.createElement('div'); + } + componentDidMount() { + const body = document.querySelector('body'); + if (body && this.el) { + body.appendChild(this.el); + } + } + + componentWillUnmount() { + const body = document.querySelector('body'); + + if (body && this.el) { + body.removeChild(this.el); + } + } + + render() { + if (this.el) { + return ReactDOM.createPortal( + , + this.el + ); + } + } +} + +export const AddEmbeddablePanel = compose void }>( + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(EmbeddableFlyoutPortal); diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts new file mode 100644 index 0000000000000..a7fac10b0c02d --- /dev/null +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EmbeddableFlyoutPortal, AddEmbeddablePanel } from './flyout'; +export { + AddEmbeddableFlyout as AddEmbeddableFlyoutComponent, + Props as AddEmbeddableFlyoutComponentProps, +} from './flyout.component'; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx deleted file mode 100644 index 62a073daf4c59..0000000000000 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { compose } from 'recompose'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { AddEmbeddableFlyout, Props } from './flyout'; -// @ts-expect-error untyped local -import { addElement } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; - -const allowedEmbeddables = { - [EmbeddableTypes.map]: (id: string) => { - return `savedMap id="${id}" | render`; - }, - [EmbeddableTypes.lens]: (id: string) => { - return `savedLens id="${id}" | render`; - }, - [EmbeddableTypes.visualization]: (id: string) => { - return `savedVisualization id="${id}" | render`; - }, - /* - [EmbeddableTypes.search]: (id: string) => { - return `filters | savedSearch id="${id}" | render`; - },*/ -}; - -interface StateProps { - pageId: string; -} - -interface DispatchProps { - addEmbeddable: (pageId: string, partialElement: { expression: string }) => void; -} - -// FIX: Missing state type -const mapStateToProps = (state: any) => ({ pageId: getSelectedPage(state) }); - -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - addEmbeddable: (pageId, partialElement): DispatchProps['addEmbeddable'] => - dispatch(addElement(pageId, partialElement)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: Props -): Props => { - const { pageId, ...remainingStateProps } = stateProps; - const { addEmbeddable } = dispatchProps; - - return { - ...remainingStateProps, - ...ownProps, - onSelect: (id: string, type: string): void => { - const partialElement = { - expression: `markdown "Could not find embeddable for type ${type}" | render`, - }; - if (allowedEmbeddables[type]) { - partialElement.expression = allowedEmbeddables[type](id); - } - - addEmbeddable(pageId, partialElement); - ownProps.onClose(); - }, - }; -}; - -export class EmbeddableFlyoutPortal extends React.Component { - el?: HTMLElement; - - constructor(props: Props) { - super(props); - - this.el = document.createElement('div'); - } - componentDidMount() { - const body = document.querySelector('body'); - if (body && this.el) { - body.appendChild(this.el); - } - } - - componentWillUnmount() { - const body = document.querySelector('body'); - - if (body && this.el) { - body.removeChild(this.el); - } - } - - render() { - if (this.el) { - return ReactDOM.createPortal( - , - this.el - ); - } - } -} - -export const AddEmbeddablePanel = compose void }>( - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(EmbeddableFlyoutPortal); diff --git a/x-pack/plugins/canvas/public/components/page_manager/index.ts b/x-pack/plugins/canvas/public/components/page_manager/index.ts index d19540cd6a687..abe7a4a3a5bb1 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/index.ts +++ b/x-pack/plugins/canvas/public/components/page_manager/index.ts @@ -4,28 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch } from 'redux'; -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; -import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; -import { PageManager as Component } from './page_manager'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - isWriteable: isWriteable(state) && canUserWrite(state), - pages: getPages(state), - selectedPage: getSelectedPage(state), - workpadId: getWorkpad(state).id, - workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onAddPage: () => dispatch(pageActions.addPage()), - onMovePage: (id: string, position: number) => dispatch(pageActions.movePage(id, position)), - onRemovePage: (id: string) => dispatch(pageActions.removePage(id)), -}); - -export const PageManager = connect(mapStateToProps, mapDispatchToProps)(Component); +export { PageManager } from './page_manager'; +export { PageManager as PageManagerComponent } from './page_manager.component'; diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx rename to x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts b/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts new file mode 100644 index 0000000000000..a92f7c6b4c352 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; +import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; +import { PageManager as Component } from './page_manager.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + pages: getPages(state), + selectedPage: getSelectedPage(state), + workpadId: getWorkpad(state).id, + workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onAddPage: () => dispatch(pageActions.addPage()), + onMovePage: (id: string, position: number) => dispatch(pageActions.movePage(id, position)), + onRemovePage: (id: string) => dispatch(pageActions.removePage(id)), +}); + +export const PageManager = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/page_preview/index.ts b/x-pack/plugins/canvas/public/components/page_preview/index.ts index 25d3254595d2e..22e3861eb9652 100644 --- a/x-pack/plugins/canvas/public/components/page_preview/index.ts +++ b/x-pack/plugins/canvas/public/components/page_preview/index.ts @@ -4,21 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch } from 'redux'; -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { isWriteable } from '../../state/selectors/workpad'; -import { PagePreview as Component } from './page_preview'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - isWriteable: isWriteable(state) && canUserWrite(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onDuplicate: (id: string) => dispatch(pageActions.duplicatePage(id)), -}); - -export const PagePreview = connect(mapStateToProps, mapDispatchToProps)(Component); +export { PagePreview } from './page_preview'; +export { PagePreview as PagePreviewComponent } from './page_preview.component'; diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx b/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx rename to x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts b/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts new file mode 100644 index 0000000000000..8768a2fc169ef --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_preview/page_preview.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 { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { isWriteable } from '../../state/selectors/workpad'; +import { PagePreview as Component } from './page_preview.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onDuplicate: (id: string) => dispatch(pageActions.duplicatePage(id)), +}); + +export const PagePreview = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx index 4941d8cb2efa7..a811a296f2e7b 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { SavedElementsModal } from '../saved_elements_modal'; +import { SavedElementsModal } from '../saved_elements_modal.component'; import { testCustomElements } from './fixtures/test_elements'; import { CustomElement } from '../../../../types'; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts index da2955c146193..46faf8d14f9b5 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts @@ -4,130 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { compose, withState } from 'recompose'; -import { camelCase } from 'lodash'; -import { cloneSubgraphs } from '../../lib/clone_subgraphs'; -import * as customElementService from '../../lib/custom_element_service'; -import { withServices, WithServicesProps } from '../../services'; -// @ts-expect-error untyped local -import { selectToplevelNodes } from '../../state/actions/transient'; -// @ts-expect-error untyped local -import { insertNodes } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; -import { SavedElementsModal as Component, Props as ComponentProps } from './saved_elements_modal'; -import { State, PositionedElement, CustomElement } from '../../../types'; - -const customElementAdded = 'elements-custom-added'; - -interface OwnProps { - onClose: () => void; -} - -interface OwnPropsWithState extends OwnProps { - customElements: CustomElement[]; - setCustomElements: (customElements: CustomElement[]) => void; - search: string; - setSearch: (search: string) => void; -} - -interface DispatchProps { - selectToplevelNodes: (nodes: PositionedElement[]) => void; - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; -} - -interface StateProps { - pageId: string; -} - -const mapStateToProps = (state: State): StateProps => ({ - pageId: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - selectToplevelNodes: (nodes: PositionedElement[]) => - dispatch( - selectToplevelNodes( - nodes - .filter((e: PositionedElement): boolean => !e.position.parent) - .map((e: PositionedElement): string => e.id) - ) - ), - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => - dispatch(insertNodes(selectedNodes, pageId)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: OwnPropsWithState & WithServicesProps -): ComponentProps => { - const { pageId } = stateProps; - const { onClose, search, setCustomElements } = ownProps; - - const findCustomElements = async () => { - const { customElements } = await customElementService.find(search); - setCustomElements(customElements); - }; - - return { - ...ownProps, - // add custom element to the page - addCustomElement: (customElement: CustomElement) => { - const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; - const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); - if (clonedNodes) { - dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) - dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) - } - onClose(); - trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); - }, - // custom element search - findCustomElements: async (text?: string) => { - try { - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't find custom elements`, - }); - } - }, - // remove custom element - removeCustomElement: async (id: string) => { - try { - await customElementService.remove(id); - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't delete custom elements`, - }); - } - }, - // update custom element - updateCustomElement: async (id: string, name: string, description: string, image: string) => { - try { - await customElementService.update(id, { - name: camelCase(name), - displayName: name, - image, - help: description, - }); - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't update custom elements`, - }); - } - }, - }; -}; - -export const SavedElementsModal = compose( - withServices, - withState('search', 'setSearch', ''), - withState('customElements', 'setCustomElements', []), - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(Component); +export { SavedElementsModal } from './saved_elements_modal'; +export { + SavedElementsModal as SavedElementsModalComponent, + Props as SavedElementsModalComponentProps, +} from './saved_elements_modal.component'; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts new file mode 100644 index 0000000000000..a5c5a2e0adce9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { compose, withState } from 'recompose'; +import { camelCase } from 'lodash'; +import { cloneSubgraphs } from '../../lib/clone_subgraphs'; +import * as customElementService from '../../lib/custom_element_service'; +import { withServices, WithServicesProps } from '../../services'; +// @ts-expect-error untyped local +import { selectToplevelNodes } from '../../state/actions/transient'; +// @ts-expect-error untyped local +import { insertNodes } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; +import { + SavedElementsModal as Component, + Props as ComponentProps, +} from './saved_elements_modal.component'; +import { State, PositionedElement, CustomElement } from '../../../types'; + +const customElementAdded = 'elements-custom-added'; + +interface OwnProps { + onClose: () => void; +} + +interface OwnPropsWithState extends OwnProps { + customElements: CustomElement[]; + setCustomElements: (customElements: CustomElement[]) => void; + search: string; + setSearch: (search: string) => void; +} + +interface DispatchProps { + selectToplevelNodes: (nodes: PositionedElement[]) => void; + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; +} + +interface StateProps { + pageId: string; +} + +const mapStateToProps = (state: State): StateProps => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes( + nodes + .filter((e: PositionedElement): boolean => !e.position.parent) + .map((e: PositionedElement): string => e.id) + ) + ), + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: OwnPropsWithState & WithServicesProps +): ComponentProps => { + const { pageId } = stateProps; + const { onClose, search, setCustomElements } = ownProps; + + const findCustomElements = async () => { + const { customElements } = await customElementService.find(search); + setCustomElements(customElements); + }; + + return { + ...ownProps, + // add custom element to the page + addCustomElement: (customElement: CustomElement) => { + const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; + const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); + if (clonedNodes) { + dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) + dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) + } + onClose(); + trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); + }, + // custom element search + findCustomElements: async (text?: string) => { + try { + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't find custom elements`, + }); + } + }, + // remove custom element + removeCustomElement: async (id: string) => { + try { + await customElementService.remove(id); + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't delete custom elements`, + }); + } + }, + // update custom element + updateCustomElement: async (id: string, name: string, description: string, image: string) => { + try { + await customElementService.update(id, { + name: camelCase(name), + displayName: name, + image, + help: description, + }); + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't update custom elements`, + }); + } + }, + }; +}; + +export const SavedElementsModal = compose( + withServices, + withState('search', 'setSearch', ''), + withState('customElements', 'setCustomElements', []), + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx new file mode 100644 index 0000000000000..e3f4e00f4de01 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiTabbedContent } from '@elastic/eui'; +// @ts-expect-error unconverted component +import { Datasource } from '../../datasource'; +// @ts-expect-error unconverted component +import { FunctionFormList } from '../../function_form_list'; +import { PositionedElement } from '../../../../types'; +import { ComponentStrings } from '../../../../i18n'; + +interface Props { + /** + * a Canvas element used to populate config forms + */ + element: PositionedElement; +} + +const { ElementSettings: strings } = ComponentStrings; + +export const ElementSettings: FunctionComponent = ({ element }) => { + const tabs = [ + { + id: 'edit', + name: strings.getDisplayTabLabel(), + content: ( +
+
+ +
+
+ ), + }, + { + id: 'data', + name: strings.getDataTabLabel(), + content: ( +
+ +
+ ), + }, + ]; + + return ; +}; + +ElementSettings.propTypes = { + element: PropTypes.object, +}; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx index e3f4e00f4de01..ba7e31a25daba 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx @@ -3,53 +3,32 @@ * 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, { FunctionComponent } from 'react'; -import PropTypes from 'prop-types'; -import { EuiTabbedContent } from '@elastic/eui'; -// @ts-expect-error unconverted component -import { Datasource } from '../../datasource'; -// @ts-expect-error unconverted component -import { FunctionFormList } from '../../function_form_list'; -import { PositionedElement } from '../../../../types'; -import { ComponentStrings } from '../../../../i18n'; +import React from 'react'; +import { connect } from 'react-redux'; +import { getElementById, getSelectedPage } from '../../../state/selectors/workpad'; +import { ElementSettings as Component } from './element_settings.component'; +import { State, PositionedElement } from '../../../../types'; interface Props { - /** - * a Canvas element used to populate config forms - */ - element: PositionedElement; + selectedElementId: string; } -const { ElementSettings: strings } = ComponentStrings; +const mapStateToProps = (state: State, { selectedElementId }: Props): StateProps => ({ + element: getElementById(state, selectedElementId, getSelectedPage(state)), +}); + +interface StateProps { + element: PositionedElement | undefined; +} -export const ElementSettings: FunctionComponent = ({ element }) => { - const tabs = [ - { - id: 'edit', - name: strings.getDisplayTabLabel(), - content: ( -
-
- -
-
- ), - }, - { - id: 'data', - name: strings.getDataTabLabel(), - content: ( -
- -
- ), - }, - ]; +const renderIfElement: React.FunctionComponent = (props) => { + if (props.element) { + return ; + } - return ; + return null; }; -ElementSettings.propTypes = { - element: PropTypes.object, -}; +export const ElementSettings = connect(mapStateToProps)( + renderIfElement +); diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts new file mode 100644 index 0000000000000..68b90f232fb8b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ElementSettings } from './element_settings'; +export { ElementSettings as ElementSettingsComponent } from './element_settings.component'; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx deleted file mode 100644 index b8d5882234899..0000000000000 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { connect } from 'react-redux'; -import { getElementById, getSelectedPage } from '../../../state/selectors/workpad'; -import { ElementSettings as Component } from './element_settings'; -import { State, PositionedElement } from '../../../../types'; - -interface Props { - selectedElementId: string; -} - -const mapStateToProps = (state: State, { selectedElementId }: Props): StateProps => ({ - element: getElementById(state, selectedElementId, getSelectedPage(state)), -}); - -interface StateProps { - element: PositionedElement | undefined; -} - -const renderIfElement: React.FunctionComponent = (props) => { - if (props.element) { - return ; - } - - return null; -}; - -export const ElementSettings = connect(mapStateToProps)( - renderIfElement -); diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts b/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts index abd40731078ec..34e3d3ff4b057 100644 --- a/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts @@ -4,20 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { addColor, removeColor } from '../../state/actions/workpad'; -import { getWorkpadColors } from '../../state/selectors/workpad'; - -import { WorkpadColorPicker as Component } from '../workpad_color_picker/workpad_color_picker'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - colors: getWorkpadColors(state), -}); - -const mapDispatchToProps = { - onAddColor: addColor, - onRemoveColor: removeColor, -}; - -export const WorkpadColorPicker = connect(mapStateToProps, mapDispatchToProps)(Component); +export { WorkpadColorPicker } from './workpad_color_picker'; +export { WorkpadColorPicker as WorkpadColorPickerComponent } from './workpad_color_picker.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx rename to x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts new file mode 100644 index 0000000000000..2f4b0fe7b4ec1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { addColor, removeColor } from '../../state/actions/workpad'; +import { getWorkpadColors } from '../../state/selectors/workpad'; + +import { WorkpadColorPicker as Component } from '../workpad_color_picker/workpad_color_picker.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + colors: getWorkpadColors(state), +}); + +const mapDispatchToProps = { + onAddColor: addColor, + onRemoveColor: removeColor, +}; + +export const WorkpadColorPicker = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_config/index.ts b/x-pack/plugins/canvas/public/components/workpad_config/index.ts index bba08d7647e9e..63db96ca5aef9 100644 --- a/x-pack/plugins/canvas/public/components/workpad_config/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_config/index.ts @@ -4,40 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; - -import { get } from 'lodash'; -import { - sizeWorkpad as setSize, - setName, - setWorkpadCSS, - updateWorkpadVariables, -} from '../../state/actions/workpad'; - -import { getWorkpad } from '../../state/selectors/workpad'; -import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; -import { WorkpadConfig as Component } from './workpad_config'; -import { State, CanvasVariable } from '../../../types'; - -const mapStateToProps = (state: State) => { - const workpad = getWorkpad(state); - - return { - name: get(workpad, 'name'), - size: { - width: get(workpad, 'width'), - height: get(workpad, 'height'), - }, - css: get(workpad, 'css', DEFAULT_WORKPAD_CSS), - variables: get(workpad, 'variables', []), - }; -}; - -const mapDispatchToProps = { - setSize, - setName, - setWorkpadCSS, - setWorkpadVariables: (vars: CanvasVariable[]) => updateWorkpadVariables(vars), -}; - -export const WorkpadConfig = connect(mapStateToProps, mapDispatchToProps)(Component); +export { WorkpadConfig } from './workpad_config'; +export { WorkpadConfig as WorkpadConfigComponent } from './workpad_config.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.tsx b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_config/workpad_config.tsx rename to x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts new file mode 100644 index 0000000000000..e4ddf31141972 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; + +import { get } from 'lodash'; +import { + sizeWorkpad as setSize, + setName, + setWorkpadCSS, + updateWorkpadVariables, +} from '../../state/actions/workpad'; + +import { getWorkpad } from '../../state/selectors/workpad'; +import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; +import { WorkpadConfig as Component } from './workpad_config.component'; +import { State, CanvasVariable } from '../../../types'; + +const mapStateToProps = (state: State) => { + const workpad = getWorkpad(state); + + return { + name: get(workpad, 'name'), + size: { + width: get(workpad, 'width'), + height: get(workpad, 'height'), + }, + css: get(workpad, 'css', DEFAULT_WORKPAD_CSS), + variables: get(workpad, 'variables', []), + }; +}; + +const mapDispatchToProps = { + setSize, + setName, + setWorkpadCSS, + setWorkpadVariables: (vars: CanvasVariable[]) => updateWorkpadVariables(vars), +}; + +export const WorkpadConfig = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx index 8bbc3e09af4bf..be6247b0bbcab 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { EditMenu } from '../edit_menu'; +import { EditMenu } from '../edit_menu.component'; import { PositionedElement } from '../../../../../types'; const handlers = { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts new file mode 100644 index 0000000000000..3a2264c05eb4b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withHandlers, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { State, PositionedElement } from '../../../../types'; +import { getClipboardData } from '../../../lib/clipboard'; +// @ts-expect-error untyped local +import { flatten } from '../../../lib/aeroelastic/functional'; +// @ts-expect-error untyped local +import { globalStateUpdater } from '../../workpad_page/integration_utils'; +// @ts-expect-error untyped local +import { crawlTree } from '../../workpad_page/integration_utils'; +// @ts-expect-error untyped local +import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements'; +// @ts-expect-error untyped local +import { undoHistory, redoHistory } from '../../../state/actions/history'; +// @ts-expect-error untyped local +import { selectToplevelNodes } from '../../../state/actions/transient'; +import { + getSelectedPage, + getNodes, + getSelectedToplevelNodes, +} from '../../../state/selectors/workpad'; +import { + layerHandlerCreators, + clipboardHandlerCreators, + basicHandlerCreators, + groupHandlerCreators, + alignmentDistributionHandlerCreators, +} from '../../../lib/element_handler_creators'; +import { EditMenu as Component, Props as ComponentProps } from './edit_menu.component'; + +type LayoutState = any; + +type CommitFn = (type: string, payload: any) => LayoutState; + +interface OwnProps { + commit: CommitFn; +} + +const withGlobalState = ( + commit: CommitFn, + updateGlobalState: (layoutState: LayoutState) => void +) => (type: string, payload: any) => { + const newLayoutState = commit(type, payload); + if (newLayoutState.currentScene.gestureEnd) { + updateGlobalState(newLayoutState); + } +}; + +/* + * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts + */ +const mapStateToProps = (state: State) => { + const pageId = getSelectedPage(state); + const nodes = getNodes(state, pageId) as PositionedElement[]; + const selectedToplevelNodes = getSelectedToplevelNodes(state); + + const selectedPrimaryShapeObjects = selectedToplevelNodes + .map((id: string) => nodes.find((s: PositionedElement) => s.id === id)) + .filter((shape?: PositionedElement) => shape) as PositionedElement[]; + + const selectedPersistentPrimaryNodes = flatten( + selectedPrimaryShapeObjects.map((shape: PositionedElement) => + nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group? + ? [shape.id] + : nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map((s) => s.id) + ) + ); + + const selectedNodeIds: string[] = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); + const selectedNodes = selectedNodeIds + .map((id: string) => nodes.find((s) => s.id === id)) + .filter((node: PositionedElement | undefined): node is PositionedElement => { + return !!node; + }); + + return { + pageId, + selectedToplevelNodes, + selectedNodes, + state, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), + removeNodes: (nodeIds: string[], pageId: string) => dispatch(removeElements(nodeIds, pageId)), + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes( + nodes.filter((e: PositionedElement) => !e.position.parent).map((e) => e.id) + ) + ), + elementLayer: (pageId: string, elementId: string, movement: number) => { + dispatch(elementLayer({ pageId, elementId, movement })); + }, + undoHistory: () => dispatch(undoHistory()), + redoHistory: () => dispatch(redoHistory()), + dispatch, +}); + +const mergeProps = ( + { state, selectedToplevelNodes, ...restStateProps }: ReturnType, + { dispatch, ...restDispatchProps }: ReturnType, + { commit }: OwnProps +) => { + const updateGlobalState = globalStateUpdater(dispatch, state); + + return { + ...restDispatchProps, + ...restStateProps, + commit: withGlobalState(commit, updateGlobalState), + groupIsSelected: + selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'), + }; +}; + +export const EditMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })), + withHandlers(basicHandlerCreators), + withHandlers(clipboardHandlerCreators), + withHandlers(layerHandlerCreators), + withHandlers(groupHandlerCreators), + withHandlers(alignmentDistributionHandlerCreators) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts index 8f013f70aefcd..0db425f01cccd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts @@ -4,130 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withHandlers, withProps } from 'recompose'; -import { Dispatch } from 'redux'; -import { State, PositionedElement } from '../../../../types'; -import { getClipboardData } from '../../../lib/clipboard'; -// @ts-expect-error untyped local -import { flatten } from '../../../lib/aeroelastic/functional'; -// @ts-expect-error untyped local -import { globalStateUpdater } from '../../workpad_page/integration_utils'; -// @ts-expect-error untyped local -import { crawlTree } from '../../workpad_page/integration_utils'; -// @ts-expect-error untyped local -import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements'; -// @ts-expect-error untyped local -import { undoHistory, redoHistory } from '../../../state/actions/history'; -// @ts-expect-error untyped local -import { selectToplevelNodes } from '../../../state/actions/transient'; -import { - getSelectedPage, - getNodes, - getSelectedToplevelNodes, -} from '../../../state/selectors/workpad'; -import { - layerHandlerCreators, - clipboardHandlerCreators, - basicHandlerCreators, - groupHandlerCreators, - alignmentDistributionHandlerCreators, -} from '../../../lib/element_handler_creators'; -import { EditMenu as Component, Props as ComponentProps } from './edit_menu'; - -type LayoutState = any; - -type CommitFn = (type: string, payload: any) => LayoutState; - -interface OwnProps { - commit: CommitFn; -} - -const withGlobalState = ( - commit: CommitFn, - updateGlobalState: (layoutState: LayoutState) => void -) => (type: string, payload: any) => { - const newLayoutState = commit(type, payload); - if (newLayoutState.currentScene.gestureEnd) { - updateGlobalState(newLayoutState); - } -}; - -/* - * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts - */ -const mapStateToProps = (state: State) => { - const pageId = getSelectedPage(state); - const nodes = getNodes(state, pageId) as PositionedElement[]; - const selectedToplevelNodes = getSelectedToplevelNodes(state); - - const selectedPrimaryShapeObjects = selectedToplevelNodes - .map((id: string) => nodes.find((s: PositionedElement) => s.id === id)) - .filter((shape?: PositionedElement) => shape) as PositionedElement[]; - - const selectedPersistentPrimaryNodes = flatten( - selectedPrimaryShapeObjects.map((shape: PositionedElement) => - nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group? - ? [shape.id] - : nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map((s) => s.id) - ) - ); - - const selectedNodeIds: string[] = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); - const selectedNodes = selectedNodeIds - .map((id: string) => nodes.find((s) => s.id === id)) - .filter((node: PositionedElement | undefined): node is PositionedElement => { - return !!node; - }); - - return { - pageId, - selectedToplevelNodes, - selectedNodes, - state, - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => - dispatch(insertNodes(selectedNodes, pageId)), - removeNodes: (nodeIds: string[], pageId: string) => dispatch(removeElements(nodeIds, pageId)), - selectToplevelNodes: (nodes: PositionedElement[]) => - dispatch( - selectToplevelNodes( - nodes.filter((e: PositionedElement) => !e.position.parent).map((e) => e.id) - ) - ), - elementLayer: (pageId: string, elementId: string, movement: number) => { - dispatch(elementLayer({ pageId, elementId, movement })); - }, - undoHistory: () => dispatch(undoHistory()), - redoHistory: () => dispatch(redoHistory()), - dispatch, -}); - -const mergeProps = ( - { state, selectedToplevelNodes, ...restStateProps }: ReturnType, - { dispatch, ...restDispatchProps }: ReturnType, - { commit }: OwnProps -) => { - const updateGlobalState = globalStateUpdater(dispatch, state); - - return { - ...restDispatchProps, - ...restStateProps, - commit: withGlobalState(commit, updateGlobalState), - groupIsSelected: - selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'), - }; -}; - -export const EditMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })), - withHandlers(basicHandlerCreators), - withHandlers(clipboardHandlerCreators), - withHandlers(layerHandlerCreators), - withHandlers(groupHandlerCreators), - withHandlers(alignmentDistributionHandlerCreators) -)(Component); +export { EditMenu } from './edit_menu'; +export { EditMenu as EditMenuComponent } from './edit_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx index 9aca5ce33ba02..cf9b334ffe8ea 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx @@ -8,7 +8,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { ElementSpec } from '../../../../../types'; -import { ElementMenu } from '../element_menu'; +import { ElementMenu } from '../element_menu.component'; const testElements: { [key: string]: ElementSpec } = { areaChart: { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx new file mode 100644 index 0000000000000..6d9233aaba22b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sortBy } from 'lodash'; +import React, { Fragment, FunctionComponent, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiButton, + EuiContextMenu, + EuiIcon, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; +import { ComponentStrings } from '../../../../i18n/components'; +import { ElementSpec } from '../../../../types'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { getId } from '../../../lib/get_id'; +import { Popover, ClosePopoverFn } from '../../popover'; +import { AssetManager } from '../../asset_manager'; +import { SavedElementsModal } from '../../saved_elements_modal'; + +interface CategorizedElementLists { + [key: string]: ElementSpec[]; +} + +interface ElementTypeMeta { + [key: string]: { name: string; icon: string }; +} + +export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; + +// label and icon for the context menu item for each element type +const elementTypeMeta: ElementTypeMeta = { + chart: { name: strings.getChartMenuItemLabel(), icon: 'visArea' }, + filter: { name: strings.getFilterMenuItemLabel(), icon: 'filter' }, + image: { name: strings.getImageMenuItemLabel(), icon: 'image' }, + other: { name: strings.getOtherMenuItemLabel(), icon: 'empty' }, + progress: { name: strings.getProgressMenuItemLabel(), icon: 'visGoal' }, + shape: { name: strings.getShapeMenuItemLabel(), icon: 'node' }, + text: { name: strings.getTextMenuItemLabel(), icon: 'visText' }, +}; + +const getElementType = (element: ElementSpec): string => + element && element.type && Object.keys(elementTypeMeta).includes(element.type) + ? element.type + : 'other'; + +const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: ElementSpec[] } => { + elements = sortBy(elements, 'displayName'); + + const categories: CategorizedElementLists = { other: [] }; + + elements.forEach((element: ElementSpec) => { + const type = getElementType(element); + + if (categories[type]) { + categories[type].push(element); + } else { + categories[type] = [element]; + } + }); + + return categories; +}; + +export interface Props { + /** + * Dictionary of elements from elements registry + */ + elements: { [key: string]: ElementSpec }; + /** + * Handler for adding a selected element to the workpad + */ + addElement: (element: ElementSpec) => void; + /** + * Renders embeddable flyout + */ + renderEmbedPanel: (onClose: () => void) => JSX.Element; +} + +export const ElementMenu: FunctionComponent = ({ + elements, + addElement, + renderEmbedPanel, +}) => { + const [isAssetModalVisible, setAssetModalVisible] = useState(false); + const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); + const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); + + const hideAssetModal = () => setAssetModalVisible(false); + const showAssetModal = () => setAssetModalVisible(true); + const hideEmbedPanel = () => setEmbedPanelVisible(false); + const showEmbedPanel = () => setEmbedPanelVisible(true); + const hideSavedElementsModal = () => setSavedElementsModalVisible(false); + const showSavedElementsModal = () => setSavedElementsModalVisible(true); + + const { + chart: chartElements, + filter: filterElements, + image: imageElements, + other: otherElements, + progress: progressElements, + shape: shapeElements, + text: textElements, + } = categorizeElementsByType(Object.values(elements)); + + const getPanelTree = (closePopover: ClosePopoverFn) => { + const elementToMenuItem = (element: ElementSpec): EuiContextMenuPanelItemDescriptor => ({ + name: element.displayName || element.name, + icon: element.icon, + onClick: () => { + addElement(element); + closePopover(); + }, + }); + + const elementListToMenuItems = (elementList: ElementSpec[]) => { + const type = getElementType(elementList[0]); + const { name, icon } = elementTypeMeta[type] || elementTypeMeta.other; + + if (elementList.length > 1) { + return { + name, + icon: , + panel: { + id: getId('element-type'), + title: name, + items: elementList.map(elementToMenuItem), + }, + }; + } + + return elementToMenuItem(elementList[0]); + }; + + return { + id: 0, + items: [ + elementListToMenuItems(textElements), + elementListToMenuItems(shapeElements), + elementListToMenuItems(chartElements), + elementListToMenuItems(imageElements), + elementListToMenuItems(filterElements), + elementListToMenuItems(progressElements), + elementListToMenuItems(otherElements), + { + name: strings.getMyElementsMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + 'data-test-subj': 'saved-elements-menu-option', + icon: , + onClick: () => { + showSavedElementsModal(); + closePopover(); + }, + }, + { + name: strings.getAssetsMenuItemLabel(), + icon: , + onClick: () => { + showAssetModal(); + closePopover(); + }, + }, + { + name: strings.getEmbedObjectMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + icon: , + onClick: () => { + showEmbedPanel(); + closePopover(); + }, + }, + ], + }; + }; + + const exportControl = (togglePopover: React.MouseEventHandler) => ( + + {strings.getElementMenuButtonLabel()} + + ); + + return ( + + + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + + )} + + {isAssetModalVisible ? : null} + {isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null} + {isSavedElementsModalVisible ? : null} + + ); +}; + +ElementMenu.propTypes = { + elements: PropTypes.object, + addElement: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx index 6d9233aaba22b..2cbe4ae5a6575 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx @@ -4,211 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sortBy } from 'lodash'; -import React, { Fragment, FunctionComponent, useState } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiButton, - EuiContextMenu, - EuiIcon, - EuiContextMenuPanelItemDescriptor, -} from '@elastic/eui'; -import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; -import { ComponentStrings } from '../../../../i18n/components'; -import { ElementSpec } from '../../../../types'; -import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; -import { getId } from '../../../lib/get_id'; -import { Popover, ClosePopoverFn } from '../../popover'; -import { AssetManager } from '../../asset_manager'; -import { SavedElementsModal } from '../../saved_elements_modal'; - -interface CategorizedElementLists { - [key: string]: ElementSpec[]; -} - -interface ElementTypeMeta { - [key: string]: { name: string; icon: string }; +import React from 'react'; +import { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { State, ElementSpec } from '../../../../types'; +// @ts-expect-error untyped local +import { elementsRegistry } from '../../../lib/elements_registry'; +import { ElementMenu as Component, Props as ComponentProps } from './element_menu.component'; +// @ts-expect-error untyped local +import { addElement } from '../../../state/actions/elements'; +import { getSelectedPage } from '../../../state/selectors/workpad'; +import { AddEmbeddablePanel } from '../../embeddable_flyout'; + +interface StateProps { + pageId: string; } -export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; - -// label and icon for the context menu item for each element type -const elementTypeMeta: ElementTypeMeta = { - chart: { name: strings.getChartMenuItemLabel(), icon: 'visArea' }, - filter: { name: strings.getFilterMenuItemLabel(), icon: 'filter' }, - image: { name: strings.getImageMenuItemLabel(), icon: 'image' }, - other: { name: strings.getOtherMenuItemLabel(), icon: 'empty' }, - progress: { name: strings.getProgressMenuItemLabel(), icon: 'visGoal' }, - shape: { name: strings.getShapeMenuItemLabel(), icon: 'node' }, - text: { name: strings.getTextMenuItemLabel(), icon: 'visText' }, -}; - -const getElementType = (element: ElementSpec): string => - element && element.type && Object.keys(elementTypeMeta).includes(element.type) - ? element.type - : 'other'; - -const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: ElementSpec[] } => { - elements = sortBy(elements, 'displayName'); - - const categories: CategorizedElementLists = { other: [] }; - - elements.forEach((element: ElementSpec) => { - const type = getElementType(element); - - if (categories[type]) { - categories[type].push(element); - } else { - categories[type] = [element]; - } - }); - - return categories; -}; - -export interface Props { - /** - * Dictionary of elements from elements registry - */ - elements: { [key: string]: ElementSpec }; - /** - * Handler for adding a selected element to the workpad - */ - addElement: (element: ElementSpec) => void; - /** - * Renders embeddable flyout - */ - renderEmbedPanel: (onClose: () => void) => JSX.Element; +interface DispatchProps { + addElement: (pageId: string) => (partialElement: ElementSpec) => void; } -export const ElementMenu: FunctionComponent = ({ - elements, - addElement, - renderEmbedPanel, -}) => { - const [isAssetModalVisible, setAssetModalVisible] = useState(false); - const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); - const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); - - const hideAssetModal = () => setAssetModalVisible(false); - const showAssetModal = () => setAssetModalVisible(true); - const hideEmbedPanel = () => setEmbedPanelVisible(false); - const showEmbedPanel = () => setEmbedPanelVisible(true); - const hideSavedElementsModal = () => setSavedElementsModalVisible(false); - const showSavedElementsModal = () => setSavedElementsModalVisible(true); - - const { - chart: chartElements, - filter: filterElements, - image: imageElements, - other: otherElements, - progress: progressElements, - shape: shapeElements, - text: textElements, - } = categorizeElementsByType(Object.values(elements)); - - const getPanelTree = (closePopover: ClosePopoverFn) => { - const elementToMenuItem = (element: ElementSpec): EuiContextMenuPanelItemDescriptor => ({ - name: element.displayName || element.name, - icon: element.icon, - onClick: () => { - addElement(element); - closePopover(); - }, - }); - - const elementListToMenuItems = (elementList: ElementSpec[]) => { - const type = getElementType(elementList[0]); - const { name, icon } = elementTypeMeta[type] || elementTypeMeta.other; - - if (elementList.length > 1) { - return { - name, - icon: , - panel: { - id: getId('element-type'), - title: name, - items: elementList.map(elementToMenuItem), - }, - }; - } - - return elementToMenuItem(elementList[0]); - }; - - return { - id: 0, - items: [ - elementListToMenuItems(textElements), - elementListToMenuItems(shapeElements), - elementListToMenuItems(chartElements), - elementListToMenuItems(imageElements), - elementListToMenuItems(filterElements), - elementListToMenuItems(progressElements), - elementListToMenuItems(otherElements), - { - name: strings.getMyElementsMenuItemLabel(), - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - 'data-test-subj': 'saved-elements-menu-option', - icon: , - onClick: () => { - showSavedElementsModal(); - closePopover(); - }, - }, - { - name: strings.getAssetsMenuItemLabel(), - icon: , - onClick: () => { - showAssetModal(); - closePopover(); - }, - }, - { - name: strings.getEmbedObjectMenuItemLabel(), - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - icon: , - onClick: () => { - showEmbedPanel(); - closePopover(); - }, - }, - ], - }; - }; - - const exportControl = (togglePopover: React.MouseEventHandler) => ( - - {strings.getElementMenuButtonLabel()} - - ); - - return ( - - - {({ closePopover }: { closePopover: ClosePopoverFn }) => ( - - )} - - {isAssetModalVisible ? : null} - {isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null} - {isSavedElementsModalVisible ? : null} - - ); -}; - -ElementMenu.propTypes = { - elements: PropTypes.object, - addElement: PropTypes.func.isRequired, -}; +const mapStateToProps = (state: State) => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)), +}); + +const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ + ...stateProps, + ...dispatchProps, + addElement: dispatchProps.addElement(stateProps.pageId), + // Moved this section out of the main component to enable stories + renderEmbedPanel: (onClose: () => void) => , +}); + +export const ElementMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withProps(() => ({ elements: elementsRegistry.toJS() })) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts new file mode 100644 index 0000000000000..26f81e125f6e2 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ElementMenu } from './element_menu'; +export { ElementMenu as ElementMenuComponent } from './element_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx deleted file mode 100644 index 264873fc994dd..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { Dispatch } from 'redux'; -import { State, ElementSpec } from '../../../../types'; -// @ts-expect-error untyped local -import { elementsRegistry } from '../../../lib/elements_registry'; -import { ElementMenu as Component, Props as ComponentProps } from './element_menu'; -// @ts-expect-error untyped local -import { addElement } from '../../../state/actions/elements'; -import { getSelectedPage } from '../../../state/selectors/workpad'; -import { AddEmbeddablePanel } from '../../embeddable_flyout'; - -interface StateProps { - pageId: string; -} - -interface DispatchProps { - addElement: (pageId: string) => (partialElement: ElementSpec) => void; -} - -const mapStateToProps = (state: State) => ({ - pageId: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)), -}); - -const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ - ...stateProps, - ...dispatchProps, - addElement: dispatchProps.addElement(stateProps.pageId), - // Moved this section out of the main component to enable stories - renderEmbedPanel: (onClose: () => void) => , -}); - -export const ElementMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps(() => ({ elements: elementsRegistry.toJS() })) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/index.ts new file mode 100644 index 0000000000000..0b6f8cc06d198 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { WorkpadHeader } from './workpad_header'; +export { WorkpadHeader as WorkpadHeaderComponent } from './workpad_header.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/index.tsx deleted file mode 100644 index 407b4ff932811..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { canUserWrite } from '../../state/selectors/app'; -import { getSelectedPage, isWriteable } from '../../state/selectors/workpad'; -import { setWriteable } from '../../state/actions/workpad'; -import { State } from '../../../types'; -import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header'; - -interface StateProps { - isWriteable: boolean; - canUserWrite: boolean; - selectedPage: string; -} - -interface DispatchProps { - setWriteable: (isWorkpadWriteable: boolean) => void; -} - -const mapStateToProps = (state: State): StateProps => ({ - isWriteable: isWriteable(state) && canUserWrite(state), - canUserWrite: canUserWrite(state), - selectedPage: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: ComponentProps -): ComponentProps => ({ - ...stateProps, - ...dispatchProps, - ...ownProps, - toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), -}); - -export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts index 87b926d93ccb9..8db62f5ac2d87 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts @@ -4,19 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import { fetchAllRenderables } from '../../../state/actions/elements'; -import { getInFlight } from '../../../state/selectors/resolved_args'; -import { State } from '../../../../types'; -import { RefreshControl as Component } from './refresh_control'; - -const mapStateToProps = (state: State) => ({ - inFlight: getInFlight(state), -}); - -const mapDispatchToProps = { - doRefresh: fetchAllRenderables, -}; - -export const RefreshControl = connect(mapStateToProps, mapDispatchToProps)(Component); +export { RefreshControl } from './refresh_control'; +export { RefreshControl as RefreshControlComponent } from './refresh_control.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts new file mode 100644 index 0000000000000..a7f01e46927ce --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; +import { getInFlight } from '../../../state/selectors/resolved_args'; +import { State } from '../../../../types'; +import { RefreshControl as Component } from './refresh_control.component'; + +const mapStateToProps = (state: State) => ({ + inFlight: getInFlight(state), +}); + +const mapDispatchToProps = { + doRefresh: fetchAllRenderables, +}; + +export const RefreshControl = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx index ab9137b1676c9..e0a1f0e381fd3 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { ShareMenu } from '../share_menu'; +import { ShareMenu } from '../share_menu.component'; storiesOf('components/WorkpadHeader/ShareMenu', module).add('default', () => ( { + const renderers: string[] = []; + const expressions = getRenderedWorkpadExpressions(state); + expressions.forEach((expression) => { + if (!renderFunctionNames.includes(expression)) { + renderers.push(expression); + } + }); + + return renderers; +}; + +const mapStateToProps = (state: State) => ({ + renderedWorkpad: getRenderedWorkpad(state), + unsupportedRenderers: getUnsupportedRenderers(state), + workpad: getWorkpad(state), +}); + +interface Props { + onClose: OnCloseFn; + renderedWorkpad: CanvasRenderedWorkpad; + unsupportedRenderers: string[]; + workpad: CanvasWorkpad; +} + +export const ShareWebsiteFlyout = compose>( + connect(mapStateToProps), + withKibana, + withProps( + ({ + unsupportedRenderers, + renderedWorkpad, + onClose, + workpad, + kibana, + }: Props & WithKibanaProps): ComponentProps => ({ + unsupportedRenderers, + onClose, + onCopy: () => { + kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage()); + }, + onDownload: (type) => { + switch (type) { + case 'share': + downloadRenderedWorkpad(renderedWorkpad); + return; + case 'shareRuntime': + downloadRuntime(kibana.services.http.basePath.get()); + return; + case 'shareZip': + const basePath = kibana.services.http.basePath.get(); + arrayBufferFetch + .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) + .then((blob) => downloadZippedRuntime(blob.data)) + .catch((err: Error) => { + kibana.services.canvas.notify.error(err, { + title: strings.getShareableZipErrorTitle(workpad.name), + }); + }); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts index 1e1eac2a1dcf3..335c5dff6ed74 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts @@ -4,95 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { - getWorkpad, - getRenderedWorkpad, - getRenderedWorkpadExpressions, -} from '../../../../state/selectors/workpad'; -import { - downloadRenderedWorkpad, - downloadRuntime, - downloadZippedRuntime, -} from '../../../../lib/download_workpad'; -import { ShareWebsiteFlyout as Component, Props as ComponentProps } from './share_website_flyout'; -import { State, CanvasWorkpad } from '../../../../../types'; -import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types'; -import { arrayBufferFetch } from '../../../../../common/lib/fetch'; -import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; -import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers'; - -import { ComponentStrings } from '../../../../../i18n/components'; -import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; -import { OnCloseFn } from '../share_menu'; -import { WithKibanaProps } from '../../../../index'; -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; - -const getUnsupportedRenderers = (state: State) => { - const renderers: string[] = []; - const expressions = getRenderedWorkpadExpressions(state); - expressions.forEach((expression) => { - if (!renderFunctionNames.includes(expression)) { - renderers.push(expression); - } - }); - - return renderers; -}; - -const mapStateToProps = (state: State) => ({ - renderedWorkpad: getRenderedWorkpad(state), - unsupportedRenderers: getUnsupportedRenderers(state), - workpad: getWorkpad(state), -}); - -interface Props { - onClose: OnCloseFn; - renderedWorkpad: CanvasRenderedWorkpad; - unsupportedRenderers: string[]; - workpad: CanvasWorkpad; -} - -export const ShareWebsiteFlyout = compose>( - connect(mapStateToProps), - withKibana, - withProps( - ({ - unsupportedRenderers, - renderedWorkpad, - onClose, - workpad, - kibana, - }: Props & WithKibanaProps): ComponentProps => ({ - unsupportedRenderers, - onClose, - onCopy: () => { - kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage()); - }, - onDownload: (type) => { - switch (type) { - case 'share': - downloadRenderedWorkpad(renderedWorkpad); - return; - case 'shareRuntime': - downloadRuntime(kibana.services.http.basePath.get()); - return; - case 'shareZip': - const basePath = kibana.services.http.basePath.get(); - arrayBufferFetch - .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) - .then((blob) => downloadZippedRuntime(blob.data)) - .catch((err: Error) => { - kibana.services.canvas.notify.error(err, { - title: strings.getShareableZipErrorTitle(workpad.name), - }); - }); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); +export { ShareWebsiteFlyout } from './flyout'; +export { ShareWebsiteFlyout as ShareWebsiteFlyoutComponent } from './flyout.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx index ea8aba688b2a6..b38226bb12a23 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx @@ -9,7 +9,7 @@ import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; import { ComponentStrings } from '../../../../../i18n/components'; -import { OnDownloadFn } from './share_website_flyout'; +import { OnDownloadFn } from './flyout'; const { ShareWebsiteRuntimeStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx index 81f559651eb25..42497fcd316fe 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx @@ -19,7 +19,7 @@ import { import { ComponentStrings } from '../../../../../i18n/components'; import { Clipboard } from '../../../clipboard'; -import { OnCopyFn } from './share_website_flyout'; +import { OnCopyFn } from './flyout'; const { ShareWebsiteSnippetsStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx index 1a5884d89d066..ac4dfe6872d3c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx @@ -9,7 +9,7 @@ import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; import { ComponentStrings } from '../../../../../i18n/components'; -import { OnDownloadFn } from './share_website_flyout'; +import { OnDownloadFn } from './flyout'; const { ShareWebsiteWorkpadStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts index 01bcfebc0dba9..19dc9b668e61a 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts @@ -4,95 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { jobCompletionNotifications } from '../../../../../../plugins/reporting/public'; -import { getWorkpad, getPages } from '../../../state/selectors/workpad'; -import { getWindow } from '../../../lib/get_window'; -import { downloadWorkpad } from '../../../lib/download_workpad'; -import { ShareMenu as Component, Props as ComponentProps } from './share_menu'; -import { getPdfUrl, createPdf } from './utils'; -import { State, CanvasWorkpad } from '../../../../types'; -import { withServices, WithServicesProps } from '../../../services'; - -import { ComponentStrings } from '../../../../i18n'; - -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; - -const mapStateToProps = (state: State) => ({ - workpad: getWorkpad(state), - pageCount: getPages(state).length, -}); - -const getAbsoluteUrl = (path: string) => { - const { location } = getWindow(); - - if (!location) { - return path; - } // fallback for mocked window object - - const { protocol, hostname, port } = location; - return `${protocol}//${hostname}:${port}${path}`; -}; - -interface Props { - workpad: CanvasWorkpad; - pageCount: number; -} - -export const ShareMenu = compose( - connect(mapStateToProps), - withServices, - withProps( - ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({ - getExportUrl: (type) => { - if (type === 'pdf') { - const pdfUrl = getPdfUrl( - workpad, - { pageCount }, - services.platform.getBasePathInterface() - ); - return getAbsoluteUrl(pdfUrl); - } - - throw new Error(strings.getUnknownExportErrorMessage(type)); - }, - onCopy: (type) => { - switch (type) { - case 'pdf': - services.notify.info(strings.getCopyPDFMessage()); - break; - case 'reportingConfig': - services.notify.info(strings.getCopyReportingConfigMessage()); - break; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - onExport: (type) => { - switch (type) { - case 'pdf': - return createPdf(workpad, { pageCount }, services.platform.getBasePathInterface()) - .then(({ data }: { data: { job: { id: string } } }) => { - services.notify.info(strings.getExportPDFMessage(), { - title: strings.getExportPDFTitle(workpad.name), - }); - - // register the job so a completion notification shows up when it's ready - jobCompletionNotifications.add(data.job.id); - }) - .catch((err: Error) => { - services.notify.error(err, { - title: strings.getExportPDFErrorTitle(workpad.name), - }); - }); - case 'json': - downloadWorkpad(workpad.id); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); +export { ShareMenu } from './share_menu'; +export { ShareMenu as ShareMenuComponent } from './share_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts new file mode 100644 index 0000000000000..85c4b14a28c13 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { jobCompletionNotifications } from '../../../../../../plugins/reporting/public'; +import { getWorkpad, getPages } from '../../../state/selectors/workpad'; +import { getWindow } from '../../../lib/get_window'; +import { downloadWorkpad } from '../../../lib/download_workpad'; +import { ShareMenu as Component, Props as ComponentProps } from './share_menu.component'; +import { getPdfUrl, createPdf } from './utils'; +import { State, CanvasWorkpad } from '../../../../types'; +import { withServices, WithServicesProps } from '../../../services'; + +import { ComponentStrings } from '../../../../i18n'; + +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; + +const mapStateToProps = (state: State) => ({ + workpad: getWorkpad(state), + pageCount: getPages(state).length, +}); + +const getAbsoluteUrl = (path: string) => { + const { location } = getWindow(); + + if (!location) { + return path; + } // fallback for mocked window object + + const { protocol, hostname, port } = location; + return `${protocol}//${hostname}:${port}${path}`; +}; + +interface Props { + workpad: CanvasWorkpad; + pageCount: number; +} + +export const ShareMenu = compose( + connect(mapStateToProps), + withServices, + withProps( + ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({ + getExportUrl: (type) => { + if (type === 'pdf') { + const pdfUrl = getPdfUrl( + workpad, + { pageCount }, + services.platform.getBasePathInterface() + ); + return getAbsoluteUrl(pdfUrl); + } + + throw new Error(strings.getUnknownExportErrorMessage(type)); + }, + onCopy: (type) => { + switch (type) { + case 'pdf': + services.notify.info(strings.getCopyPDFMessage()); + break; + case 'reportingConfig': + services.notify.info(strings.getCopyReportingConfigMessage()); + break; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + onExport: (type) => { + switch (type) { + case 'pdf': + return createPdf(workpad, { pageCount }, services.platform.getBasePathInterface()) + .then(({ data }: { data: { job: { id: string } } }) => { + services.notify.info(strings.getExportPDFMessage(), { + title: strings.getExportPDFTitle(workpad.name), + }); + + // register the job so a completion notification shows up when it's ready + jobCompletionNotifications.add(data.job.id); + }) + .catch((err: Error) => { + services.notify.error(err, { + title: strings.getExportPDFErrorTitle(workpad.name), + }); + }); + case 'json': + downloadWorkpad(workpad.id); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx index 5b4de05da3a3d..6b033feb26021 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { ViewMenu } from '../view_menu'; +import { ViewMenu } from '../view_menu.component'; const handlers = { setZoomScale: action('setZoomScale'), diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts index e2a05d13b017e..167b3822fd13d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts @@ -4,97 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withHandlers } from 'recompose'; -import { Dispatch } from 'redux'; -import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; -import { State, CanvasWorkpadBoundingBox } from '../../../../types'; -// @ts-expect-error untyped local -import { fetchAllRenderables } from '../../../state/actions/elements'; -// @ts-expect-error untyped local -import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; -import { - setWriteable, - setRefreshInterval, - enableAutoplay, - setAutoplayInterval, -} from '../../../state/actions/workpad'; -import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; -import { - getWorkpadBoundingBox, - getWorkpadWidth, - getWorkpadHeight, - isWriteable, - getRefreshInterval, - getAutoplay, -} from '../../../state/selectors/workpad'; -import { ViewMenu as Component, Props as ComponentProps } from './view_menu'; -import { getFitZoomScale } from './lib/get_fit_zoom_scale'; - -interface StateProps { - zoomScale: number; - boundingBox: CanvasWorkpadBoundingBox; - workpadWidth: number; - workpadHeight: number; - isWriteable: boolean; -} - -interface DispatchProps { - setWriteable: (isWorkpadWriteable: boolean) => void; - setZoomScale: (scale: number) => void; - setFullscreen: (showFullscreen: boolean) => void; -} - -const mapStateToProps = (state: State) => { - const { enabled, interval } = getAutoplay(state); - - return { - zoomScale: getZoomScale(state), - boundingBox: getWorkpadBoundingBox(state), - workpadWidth: getWorkpadWidth(state), - workpadHeight: getWorkpadHeight(state), - isWriteable: isWriteable(state) && canUserWrite(state), - refreshInterval: getRefreshInterval(state), - autoplayEnabled: enabled, - autoplayInterval: interval, - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), - setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), - setFullscreen: (value: boolean) => { - dispatch(setFullscreen(value)); - - if (value) { - dispatch(selectToplevelNodes([])); - } - }, - doRefresh: () => dispatch(fetchAllRenderables()), - setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)), - enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(!!autoplay)), - setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: ComponentProps -): ComponentProps => { - const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; - - return { - ...remainingStateProps, - ...dispatchProps, - ...ownProps, - toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), - enterFullscreen: () => dispatchProps.setFullscreen(true), - fitToWindow: () => - dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)), - }; -}; - -export const ViewMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withHandlers(zoomHandlerCreators) -)(Component); +export { ViewMenu } from './view_menu'; +export { ViewMenu as ViewMenuComponent } from './view_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts new file mode 100644 index 0000000000000..c9650a35ea2a6 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withHandlers } from 'recompose'; +import { Dispatch } from 'redux'; +import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; +import { State, CanvasWorkpadBoundingBox } from '../../../../types'; +// @ts-expect-error untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; +// @ts-expect-error untyped local +import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; +import { + setWriteable, + setRefreshInterval, + enableAutoplay, + setAutoplayInterval, +} from '../../../state/actions/workpad'; +import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; +import { + getWorkpadBoundingBox, + getWorkpadWidth, + getWorkpadHeight, + isWriteable, + getRefreshInterval, + getAutoplay, +} from '../../../state/selectors/workpad'; +import { ViewMenu as Component, Props as ComponentProps } from './view_menu.component'; +import { getFitZoomScale } from './lib/get_fit_zoom_scale'; + +interface StateProps { + zoomScale: number; + boundingBox: CanvasWorkpadBoundingBox; + workpadWidth: number; + workpadHeight: number; + isWriteable: boolean; +} + +interface DispatchProps { + setWriteable: (isWorkpadWriteable: boolean) => void; + setZoomScale: (scale: number) => void; + setFullscreen: (showFullscreen: boolean) => void; +} + +const mapStateToProps = (state: State) => { + const { enabled, interval } = getAutoplay(state); + + return { + zoomScale: getZoomScale(state), + boundingBox: getWorkpadBoundingBox(state), + workpadWidth: getWorkpadWidth(state), + workpadHeight: getWorkpadHeight(state), + isWriteable: isWriteable(state) && canUserWrite(state), + refreshInterval: getRefreshInterval(state), + autoplayEnabled: enabled, + autoplayInterval: interval, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), + setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), + setFullscreen: (value: boolean) => { + dispatch(setFullscreen(value)); + + if (value) { + dispatch(selectToplevelNodes([])); + } + }, + doRefresh: () => dispatch(fetchAllRenderables()), + setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)), + enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(!!autoplay)), + setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => { + const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; + + return { + ...remainingStateProps, + ...dispatchProps, + ...ownProps, + toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), + enterFullscreen: () => dispatchProps.setFullscreen(true), + fitToWindow: () => + dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)), + }; +}; + +export const ViewMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withHandlers(zoomHandlerCreators) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx new file mode 100644 index 0000000000000..eb4b451896b46 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +// @ts-expect-error no @types definition +import { Shortcuts } from 'react-shortcuts'; +import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { ComponentStrings } from '../../../i18n'; +import { ToolTipShortcut } from '../tool_tip_shortcut/'; +import { RefreshControl } from './refresh_control'; +// @ts-expect-error untyped local +import { FullscreenControl } from './fullscreen_control'; +import { EditMenu } from './edit_menu'; +import { ElementMenu } from './element_menu'; +import { ShareMenu } from './share_menu'; +import { ViewMenu } from './view_menu'; + +const { WorkpadHeader: strings } = ComponentStrings; + +export interface Props { + isWriteable: boolean; + toggleWriteable: () => void; + canUserWrite: boolean; + commit: (type: string, payload: any) => any; +} + +export const WorkpadHeader: FunctionComponent = ({ + isWriteable, + canUserWrite, + toggleWriteable, + commit, +}) => { + const keyHandler = (action: string) => { + if (action === 'EDITING') { + toggleWriteable(); + } + }; + + const fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( + + {strings.getFullScreenTooltip()}{' '} + + + } + > + + + ); + + const getEditToggleToolTipText = () => { + if (!canUserWrite) { + return strings.getNoWritePermissionTooltipText(); + } + + const content = isWriteable + ? strings.getHideEditControlTooltip() + : strings.getShowEditControlTooltip(); + + return content; + }; + + const getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { + const content = getEditToggleToolTipText(); + + if (textOnly) { + return content; + } + + return ( + + {content} + + ); + }; + + return ( + + + + {isWriteable && ( + + + + )} + + + + + + + + + + + + + + + {canUserWrite && ( + + )} + + + + + + + + + {fullscreenButton} + + + + + ); +}; + +WorkpadHeader.propTypes = { + isWriteable: PropTypes.bool, + toggleWriteable: PropTypes.func, + canUserWrite: PropTypes.bool, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx index eb4b451896b46..1f630040b0c36 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx @@ -4,147 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; -import PropTypes from 'prop-types'; -// @ts-expect-error no @types definition -import { Shortcuts } from 'react-shortcuts'; -import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n'; -import { ToolTipShortcut } from '../tool_tip_shortcut/'; -import { RefreshControl } from './refresh_control'; -// @ts-expect-error untyped local -import { FullscreenControl } from './fullscreen_control'; -import { EditMenu } from './edit_menu'; -import { ElementMenu } from './element_menu'; -import { ShareMenu } from './share_menu'; -import { ViewMenu } from './view_menu'; - -const { WorkpadHeader: strings } = ComponentStrings; - -export interface Props { +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { canUserWrite } from '../../state/selectors/app'; +import { getSelectedPage, isWriteable } from '../../state/selectors/workpad'; +import { setWriteable } from '../../state/actions/workpad'; +import { State } from '../../../types'; +import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header.component'; + +interface StateProps { isWriteable: boolean; - toggleWriteable: () => void; canUserWrite: boolean; - commit: (type: string, payload: any) => any; + selectedPage: string; } -export const WorkpadHeader: FunctionComponent = ({ - isWriteable, - canUserWrite, - toggleWriteable, - commit, -}) => { - const keyHandler = (action: string) => { - if (action === 'EDITING') { - toggleWriteable(); - } - }; - - const fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( - - {strings.getFullScreenTooltip()}{' '} - - - } - > - - - ); - - const getEditToggleToolTipText = () => { - if (!canUserWrite) { - return strings.getNoWritePermissionTooltipText(); - } - - const content = isWriteable - ? strings.getHideEditControlTooltip() - : strings.getShowEditControlTooltip(); - - return content; - }; - - const getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { - const content = getEditToggleToolTipText(); - - if (textOnly) { - return content; - } - - return ( - - {content} - - ); - }; - - return ( - - - - {isWriteable && ( - - - - )} - - - - - - - - - - - - - - - {canUserWrite && ( - - )} - - - - - - - - - {fullscreenButton} - - - - - ); -}; +interface DispatchProps { + setWriteable: (isWorkpadWriteable: boolean) => void; +} -WorkpadHeader.propTypes = { - isWriteable: PropTypes.bool, - toggleWriteable: PropTypes.func, - canUserWrite: PropTypes.bool, -}; +const mapStateToProps = (state: State): StateProps => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + canUserWrite: canUserWrite(state), + selectedPage: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => ({ + ...stateProps, + ...dispatchProps, + ...ownProps, + toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), +}); + +export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.js b/x-pack/plugins/canvas/storybook/storyshots.test.js index e3a9654bb49fa..dbcbbff6398b5 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.js +++ b/x-pack/plugins/canvas/storybook/storyshots.test.js @@ -73,7 +73,7 @@ jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => { // Disabling this test due to https://github.com/elastic/eui/issues/2242 jest.mock( - '../public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories', + '../public/components/workpad_header/share_menu/flyout/__examples__/flyout.stories', () => { return 'Disabled Panel'; } From b0d51c8e0a80d34b5356bd4a5c746834a1f5c055 Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Fri, 31 Jul 2020 12:01:11 -0400 Subject: [PATCH 41/55] Resolver/test panel presence (#73889) * Test for panel presence --- .../resolver/test_utilities/simulator/index.tsx | 14 ++++++++++++++ .../public/resolver/view/clickthrough.test.tsx | 10 ++++++++++ .../public/resolver/view/panel.tsx | 2 +- .../view/panels/panel_content_process_list.tsx | 7 ++++++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 7a61427c56a3b..2a2354921a3d4 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -251,6 +251,20 @@ export class Simulator { return this.findInDOM('[data-test-subj="resolver:graph"]'); } + /** + * The outer panel container. + */ + public panelElement(): ReactWrapper { + return this.findInDOM('[data-test-subj="resolver:panel"]'); + } + + /** + * The panel content element (which may include tables, lists, other data depending on the view). + */ + public panelContentElement(): ReactWrapper { + return this.findInDOM('[data-test-subj^="resolver:panel:"]'); + } + /** * Like `this.wrapper.find` but only returns DOM nodes. */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 9cb900736677e..f339d128944cc 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -63,6 +63,16 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( expect(simulator.processNodeElements().length).toBe(3); }); + it(`should have the default "process list" panel present`, async () => { + expect(simulator.panelElement().length).toBe(1); + expect(simulator.panelContentElement().length).toBe(1); + const testSubjectName = simulator + .panelContentElement() + .getDOMNode() + .getAttribute('data-test-subj'); + expect(testSubjectName).toMatch(/process-list/g); + }); + describe("when the second child node's first button has been clicked", () => { beforeEach(() => { // Click the first button under the second child element. diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index 83d3930065da6..f378ab36bac94 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -220,7 +220,7 @@ PanelContent.displayName = 'PanelContent'; export const Panel = memo(function Event({ className }: { className?: string }) { return ( - + ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx index efb96cde431e5..8ca002ace26fe 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx @@ -187,7 +187,12 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ {showWarning && } - items={processTableView} columns={columns} sorting /> + + data-test-subj="resolver:panel:process-list" + items={processTableView} + columns={columns} + sorting + /> ); }); From 69844e45eb8247344ea913d006680deced5d3b34 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Fri, 31 Jul 2020 11:22:14 -0500 Subject: [PATCH 42/55] [ML] Add API integration testing for AD annotations (#73068) Co-authored-by: Elastic Machine --- .../apis/ml/annotations/common_jobs.ts | 58 ++++++ .../apis/ml/annotations/create_annotations.ts | 89 +++++++++ .../apis/ml/annotations/delete_annotations.ts | 91 +++++++++ .../apis/ml/annotations/get_annotations.ts | 130 +++++++++++++ .../apis/ml/annotations/index.ts | 16 ++ .../apis/ml/annotations/update_annotations.ts | 175 ++++++++++++++++++ x-pack/test/api_integration/apis/ml/index.ts | 1 + x-pack/test/functional/services/ml/api.ts | 88 +++++++++ 8 files changed, 648 insertions(+) create mode 100644 x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts diff --git a/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts new file mode 100644 index 0000000000000..873cdc5d71baa --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; + +export const commonJobConfig = { + description: 'test_job_annotation', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + { + function: 'min', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, +}; + +export const createJobConfig = (jobId: string) => { + return { ...commonJobConfig, job_id: jobId }; +}; + +export const testSetupJobConfigs = [1, 2, 3, 4].map((num) => ({ + ...commonJobConfig, + job_id: `job_annotation_${num}_${Date.now()}`, + description: `Test annotation ${num}`, +})); +export const jobIds = testSetupJobConfigs.map((j) => j.job_id); + +export const createAnnotationRequestBody = (jobId: string): Partial => { + return { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Test annotation', + job_id: jobId, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'user', + detector_index: 1, + partition_field_name: 'airline', + partition_field_value: 'AAL', + }; +}; + +export const testSetupAnnotations = testSetupJobConfigs.map((job) => + createAnnotationRequestBody(job.job_id) +); diff --git a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts new file mode 100644 index 0000000000000..14ecf1bfe524e --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; +import { createJobConfig, createAnnotationRequestBody } from './common_jobs'; +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `job_annotation_${Date.now()}`; + const testJobConfig = createJobConfig(jobId); + const annotationRequestBody = createAnnotationRequestBody(jobId); + + describe('create_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.api.createAnomalyDetectionJob(testJobConfig); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should successfully create annotations for anomaly job', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(200); + const annotationId = body._id; + + const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); + + expect(fetchedAnnotation).to.not.be(undefined); + + if (fetchedAnnotation) { + Object.keys(annotationRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]); + }); + } + expect(fetchedAnnotation?.create_username).to.eql(USER.ML_POWERUSER); + }); + + it('should successfully create annotation for user with ML read permissions', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(200); + + const annotationId = body._id; + const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); + expect(fetchedAnnotation).to.not.be(undefined); + if (fetchedAnnotation) { + Object.keys(annotationRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]); + }); + } + expect(fetchedAnnotation?.create_username).to.eql(USER.ML_VIEWER); + }); + + it('should not allow to create annotation for unauthorized user', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts new file mode 100644 index 0000000000000..4fbb26e9b5a3e --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('delete_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should delete annotation by id', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[0]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body._id).to.eql(annotationIdToDelete); + expect(body.result).to.eql('deleted'); + + await ml.api.waitForAnnotationNotToExist(annotationIdToDelete); + }); + + it('should delete annotation by id for user with viewer permission', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[1]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body._id).to.eql(annotationIdToDelete); + expect(body.result).to.eql('deleted'); + + await ml.api.waitForAnnotationNotToExist(annotationIdToDelete); + }); + + it('should not delete annotation for unauthorized user', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[2]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + + await ml.api.waitForAnnotationToExist(annotationIdToDelete); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts new file mode 100644 index 0000000000000..710473eed6901 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { omit } from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('get_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should fetch all annotations for jobId', async () => { + const requestBody = { + jobIds: [jobIds[0]], + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + [jobIds[0]].forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should fetch all annotations for multiple jobs', async () => { + const requestBody = { + jobIds, + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + jobIds.forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should fetch all annotations for user with ML read permissions', async () => { + const requestBody = { + jobIds: testSetupJobConfigs.map((j) => j.job_id), + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + jobIds.forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should not allow to fetch annotation for unauthorized user', async () => { + const requestBody = { + jobIds: testSetupJobConfigs.map((j) => j.job_id), + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/index.ts b/x-pack/test/api_integration/apis/ml/annotations/index.ts new file mode 100644 index 0000000000000..7d73ee43d4d99 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('annotations', function () { + loadTestFile(require.resolve('./create_annotations')); + loadTestFile(require.resolve('./get_annotations')); + loadTestFile(require.resolve('./delete_annotations')); + loadTestFile(require.resolve('./update_annotations')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts new file mode 100644 index 0000000000000..ba73617151120 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const commonAnnotationUpdateRequestBody: Partial = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation', + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + partition_field_name: 'airline', + partition_field_value: 'ANA', + }; + + describe('update_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should correctly update annotation by id', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[0]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(200); + + expect(body._id).to.eql(originalAnnotation._id); + expect(body.result).to.eql('updated'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + + if (updatedAnnotation) { + Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]); + }); + } + }); + + it('should correctly update annotation for user with viewer permission', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[1]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(200); + + expect(body._id).to.eql(originalAnnotation._id); + expect(body.result).to.eql('updated'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + if (updatedAnnotation) { + Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]); + }); + } + }); + + it('should not update annotation for unauthorized user', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[2]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + expect(updatedAnnotation).to.eql(originalAnnotation._source); + }); + + it('should override fields correctly', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[3]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBodyWithMissingFields: Partial = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation', + job_id: originalAnnotation._source.job_id, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + _id: originalAnnotation._id, + }; + await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBodyWithMissingFields) + .expect(200); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + if (updatedAnnotation) { + Object.keys(annotationUpdateRequestBodyWithMissingFields).forEach((key) => { + if (key !== '_id') { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql( + annotationUpdateRequestBodyWithMissingFields[field] + ); + } + }); + } + // validate missing fields in the annotationUpdateRequestBody + expect(updatedAnnotation?.partition_field_name).to.be(undefined); + expect(updatedAnnotation?.partition_field_value).to.be(undefined); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index b29bc47b50394..969f291b0d8b3 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -60,5 +60,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./data_frame_analytics')); loadTestFile(require.resolve('./filters')); loadTestFile(require.resolve('./calendars')); + loadTestFile(require.resolve('./annotations')); }); } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 9dfec3a17dec0..401a96c5c11bd 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -5,13 +5,29 @@ */ import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test/types/ftr'; +import { IndexDocumentParams } from 'elasticsearch'; import { Calendar, CalendarEvent } from '../../../../plugins/ml/server/models/calendar/index'; +import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; import { FtrProviderContext } from '../../ftr_provider_context'; import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states'; import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; export type MlApi = ProvidedType; +import { + ML_ANNOTATIONS_INDEX_ALIAS_READ, + ML_ANNOTATIONS_INDEX_ALIAS_WRITE, +} from '../../../../plugins/ml/common/constants/index_patterns'; + +interface EsIndexResult { + _index: string; + _id: string; + _version: number; + result: string; + _shards: any; + _seq_no: number; + _primary_term: number; +} export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); @@ -634,5 +650,77 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { } }); }, + + async getAnnotations(jobId: string) { + log.debug(`Fetching annotations for job '${jobId}'...`); + + const results = await es.search({ + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + body: { + query: { + match: { + job_id: jobId, + }, + }, + }, + }); + expect(results).to.not.be(undefined); + expect(results).to.have.property('hits'); + return results.hits.hits; + }, + + async getAnnotationById(annotationId: string): Promise { + log.debug(`Fetching annotation '${annotationId}'...`); + + const result = await es.search({ + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + body: { + size: 1, + query: { + match: { + _id: annotationId, + }, + }, + }, + }); + // @ts-ignore due to outdated type for hits.total + if (result.hits.total.value === 1) { + return result?.hits?.hits[0]?._source as Annotation; + } + return undefined; + }, + + async indexAnnotation(annotationRequestBody: Partial) { + log.debug(`Indexing annotation '${JSON.stringify(annotationRequestBody)}'...`); + // @ts-ignore due to outdated type for IndexDocumentParams.type + const params: IndexDocumentParams> = { + index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, + body: annotationRequestBody, + refresh: 'wait_for', + }; + const results: EsIndexResult = await es.index(params); + await this.waitForAnnotationToExist(results._id); + return results; + }, + + async waitForAnnotationToExist(annotationId: string, errorMsg?: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await this.getAnnotationById(annotationId)) !== undefined) { + return true; + } else { + throw new Error(errorMsg ?? `annotation '${annotationId}' should exist`); + } + }); + }, + + async waitForAnnotationNotToExist(annotationId: string, errorMsg?: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await this.getAnnotationById(annotationId)) === undefined) { + return true; + } else { + throw new Error(errorMsg ?? `annotation '${annotationId}' should not exist`); + } + }); + }, }; } From 1c0b77c494c7d0a85a922f84597a5c3b2ecd4da8 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 31 Jul 2020 18:59:51 +0200 Subject: [PATCH 43/55] fix: pinned filters not applied (#73825) --- .../lens/public/app_plugin/app.test.tsx | 21 +++++++++++++++++++ x-pack/plugins/lens/public/app_plugin/app.tsx | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) 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 a72f4f429a1be..b30a586487009 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -249,6 +249,27 @@ describe('Lens App', () => { expect(defaultArgs.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); }); + it('passes global filters to frame', async () => { + const args = makeDefaultArgs(); + args.editorFrame = frame; + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); + args.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => { + return [pinnedFilter]; + }); + const component = mount(); + component.update(); + expect(frame.mount).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + dateRange: { fromDate: 'now-7d', toDate: 'now' }, + query: { query: '', language: 'kuery' }, + filters: [pinnedFilter], + }) + ); + }); + it('sets breadcrumbs when the document title changes', async () => { const defaultArgs = makeDefaultArgs(); instance = mount(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 2a7eaff32fa08..ab4c4820315ac 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -94,7 +94,7 @@ export function App({ toDate: currentRange.to, }, originatingApp, - filters: [], + filters: data.query.filterManager.getFilters(), indicateNoData: false, }; }); From 002e4598a5de5e2e65d99b1f52d069a01c3ffbd9 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 31 Jul 2020 14:04:31 -0400 Subject: [PATCH 44/55] [Ingest Manager] Revert fleet config concurrency rollout to rate limit (#73940) --- .../ingest_manager/common/types/index.ts | 3 +- x-pack/plugins/ingest_manager/server/index.ts | 3 +- .../agents/checkin/rxjs_utils.test.ts | 45 ----------------- .../services/agents/checkin/rxjs_utils.ts | 50 +++++++++++++------ .../agents/checkin/state_new_actions.ts | 9 ++-- 5 files changed, 43 insertions(+), 67 deletions(-) delete mode 100644 x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 7acef263f973a..69bcc498c18be 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -22,7 +22,8 @@ export interface IngestManagerConfigType { host?: string; ca_sha256?: string; }; - agentConfigRolloutConcurrency: number; + agentConfigRolloutRateLimitIntervalMs: number; + agentConfigRolloutRateLimitRequestPerInterval: number; }; } diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 6f8c4948559d3..e2f659f54d625 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -35,7 +35,8 @@ export const config = { host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), }), - agentConfigRolloutConcurrency: schema.number({ defaultValue: 10 }), + agentConfigRolloutRateLimitIntervalMs: schema.number({ defaultValue: 5000 }), + agentConfigRolloutRateLimitRequestPerInterval: schema.number({ defaultValue: 5 }), }), }), }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts deleted file mode 100644 index 70207dcf325c4..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as Rx from 'rxjs'; -import { share } from 'rxjs/operators'; -import { createSubscriberConcurrencyLimiter } from './rxjs_utils'; - -function createSpyObserver(o: Rx.Observable): [Rx.Subscription, jest.Mock] { - const spy = jest.fn(); - const observer = o.subscribe(spy); - return [observer, spy]; -} - -describe('createSubscriberConcurrencyLimiter', () => { - it('should not publish to more than n concurrent subscriber', async () => { - const subject = new Rx.Subject(); - const sharedObservable = subject.pipe(share()); - - const limiter = createSubscriberConcurrencyLimiter(2); - - const [observer1, spy1] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer2, spy2] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer3, spy3] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer4, spy4] = createSpyObserver(sharedObservable.pipe(limiter())); - subject.next('test1'); - - expect(spy1).toBeCalled(); - expect(spy2).toBeCalled(); - expect(spy3).not.toBeCalled(); - expect(spy4).not.toBeCalled(); - - observer1.unsubscribe(); - expect(spy3).toBeCalled(); - expect(spy4).not.toBeCalled(); - - observer2.unsubscribe(); - expect(spy4).toBeCalled(); - - observer3.unsubscribe(); - observer4.unsubscribe(); - }); -}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts index dc0ed35207e46..dddade6841460 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts @@ -43,23 +43,37 @@ export const toPromiseAbortable = ( } }); -export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { - let observers: Array<[Rx.Subscriber, any]> = []; - let activeObservers: Array> = []; +export function createRateLimiter( + ratelimitIntervalMs: number, + ratelimitRequestPerInterval: number +) { + function createCurrentInterval() { + return { + startedAt: Rx.asyncScheduler.now(), + numRequests: 0, + }; + } - function processNext() { - if (activeObservers.length >= maxConcurrency) { - return; - } - const observerValuePair = observers.shift(); + let currentInterval: { startedAt: number; numRequests: number } = createCurrentInterval(); + let observers: Array<[Rx.Subscriber, any]> = []; + let timerSubscription: Rx.Subscription | undefined; - if (!observerValuePair) { + function createTimeout() { + if (timerSubscription) { return; } - - const [observer, value] = observerValuePair; - activeObservers.push(observer); - observer.next(value); + timerSubscription = Rx.asyncScheduler.schedule(() => { + timerSubscription = undefined; + currentInterval = createCurrentInterval(); + for (const [waitingObserver, value] of observers) { + if (currentInterval.numRequests >= ratelimitRequestPerInterval) { + createTimeout(); + continue; + } + currentInterval.numRequests++; + waitingObserver.next(value); + } + }, ratelimitIntervalMs); } return function limit(): Rx.MonoTypeOperatorFunction { @@ -67,8 +81,14 @@ export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { new Rx.Observable((observer) => { const subscription = observable.subscribe({ next(value) { + if (currentInterval.numRequests < ratelimitRequestPerInterval) { + currentInterval.numRequests++; + observer.next(value); + return; + } + observers = [...observers, [observer, value]]; - processNext(); + createTimeout(); }, error(err) { observer.error(err); @@ -79,10 +99,8 @@ export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { }); return () => { - activeObservers = activeObservers.filter((o) => o !== observer); observers = observers.filter((o) => o[0] !== observer); subscription.unsubscribe(); - processNext(); }; }); }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index 53270afe453c4..1547b6b5ea053 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -28,7 +28,7 @@ import * as APIKeysService from '../../api_keys'; import { AGENT_SAVED_OBJECT_TYPE, AGENT_UPDATE_ACTIONS_INTERVAL_MS } from '../../../constants'; import { createAgentAction, getNewActionsSince } from '../actions'; import { appContextService } from '../../app_context'; -import { toPromiseAbortable, AbortError, createSubscriberConcurrencyLimiter } from './rxjs_utils'; +import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -134,8 +134,9 @@ export function agentCheckinStateNewActionsFactory() { const agentConfigs$ = new Map>(); const newActions$ = createNewActionsSharedObservable(); // Rx operators - const concurrencyLimiter = createSubscriberConcurrencyLimiter( - appContextService.getConfig()?.fleet.agentConfigRolloutConcurrency ?? 10 + const rateLimiter = createRateLimiter( + appContextService.getConfig()?.fleet.agentConfigRolloutRateLimitIntervalMs ?? 5000, + appContextService.getConfig()?.fleet.agentConfigRolloutRateLimitRequestPerInterval ?? 50 ); async function subscribeToNewActions( @@ -158,7 +159,7 @@ export function agentCheckinStateNewActionsFactory() { const stream$ = agentConfig$.pipe( timeout(appContextService.getConfig()?.fleet.pollingRequestTimeout || 0), filter((config) => shouldCreateAgentConfigAction(agent, config)), - concurrencyLimiter(), + rateLimiter(), mergeMap((config) => createAgentActionFromConfig(soClient, agent, config)), merge(newActions$), mergeMap(async (data) => { From 1f2c01531945890a1d944e7d18c16af2d1a29af7 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 31 Jul 2020 14:05:43 -0400 Subject: [PATCH 45/55] Fix a typo. (#73948) --- x-pack/plugins/ingest_manager/dev_docs/definitions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/dev_docs/definitions.md b/x-pack/plugins/ingest_manager/dev_docs/definitions.md index a33d95f3afa38..bd20780611055 100644 --- a/x-pack/plugins/ingest_manager/dev_docs/definitions.md +++ b/x-pack/plugins/ingest_manager/dev_docs/definitions.md @@ -10,7 +10,7 @@ This section is to define terms used across ingest management. A package config is a definition on how to collect data from a service, for example `nginx`. A package config contains definitions for one or multiple inputs and each input can contain one or multiple streams. -With the example of the nginx Package Config, it contains to inputs: `logs` and `nginx/metrics`. Logs and metrics are collected +With the example of the nginx Package Config, it contains two inputs: `logs` and `nginx/metrics`. Logs and metrics are collected differently. The `logs` input contains two streams, `access` and `error`, the `nginx/metrics` input contains the stubstatus stream. ## Data Stream From d9efd265c94283c9a021fa91830f2ef21cedb194 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Fri, 31 Jul 2020 13:22:40 -0500 Subject: [PATCH 46/55] [build/sysv] fix missing env variable rename (#73977) --- .../tasks/os_packages/service_templates/sysv/etc/init.d/kibana | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana index 8facbb709cc5c..449fc4e75fce8 100755 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana @@ -22,7 +22,7 @@ pidfile="/var/run/kibana/$name.pid" [ -r /etc/default/$name ] && . /etc/default/$name [ -r /etc/sysconfig/$name ] && . /etc/sysconfig/$name -export KIBANA_PATH_CONF +export KBN_PATH_CONF export NODE_OPTIONS [ -z "$nice" ] && nice=0 From 357139d67c476d7dbe28413870c21fb8ff6f1190 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 31 Jul 2020 14:55:29 -0400 Subject: [PATCH 47/55] [Ingest Manager] Fix limited concurrency helper (#73976) --- .../ingest_manager/server/routes/limited_concurrency.test.ts | 2 +- .../ingest_manager/server/routes/limited_concurrency.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts index f84f417ce402d..e5b5a83743287 100644 --- a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts @@ -39,7 +39,7 @@ describe('registerLimitedConcurrencyRoutes', () => { }); // assertions for calls to .decrease are commented out because it's called on the -// "req.events.aborted$ observable (which) will never emit from a mocked request in a jest unit test environment" +// "req.events.completed$ observable (which) will never emit from a mocked request in a jest unit test environment" // https://github.com/elastic/kibana/pull/72338#issuecomment-661908791 describe('preAuthHandler', () => { test(`ignores routes when !isMatch`, async () => { diff --git a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts index 11fdc944e031d..7ba8e151b726c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts +++ b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts @@ -66,9 +66,7 @@ export function createLimitedPreAuthHandler({ maxCounter.increase(); - // requests.events.aborted$ has a bug (but has test which explicitly verifies) where it's fired even when the request completes - // https://github.com/elastic/kibana/pull/70495#issuecomment-656288766 - request.events.aborted$.toPromise().then(() => { + request.events.completed$.toPromise().then(() => { maxCounter.decrease(); }); From 17e8d18a40a032802fc1947d18ae4d799ea1925f Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Fri, 31 Jul 2020 12:03:09 -0700 Subject: [PATCH 48/55] [APM] docs: Update machine learning integration (#73597) --- docs/apm/images/apm-anomaly-alert.png | Bin 0 -> 62561 bytes docs/apm/machine-learning.asciidoc | 49 +++++++++++++++++++------- 2 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 docs/apm/images/apm-anomaly-alert.png diff --git a/docs/apm/images/apm-anomaly-alert.png b/docs/apm/images/apm-anomaly-alert.png new file mode 100644 index 0000000000000000000000000000000000000000..35ce9a2296c9c965cc383740ff4a5cb9fb10cdbe GIT binary patch literal 62561 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 aXE?dYgZea;GI0d>+`FT5JNK6L^Zx@%rUa+} literal 0 HcmV?d00001 diff --git a/docs/apm/machine-learning.asciidoc b/docs/apm/machine-learning.asciidoc index b203b8668072f..db2a1ef6e2da0 100644 --- a/docs/apm/machine-learning.asciidoc +++ b/docs/apm/machine-learning.asciidoc @@ -1,36 +1,61 @@ [role="xpack"] [[machine-learning-integration]] -=== integration +=== Machine learning integration ++++ Integrate with machine learning ++++ -The Machine Learning integration initiates a new job predefined to calculate anomaly scores on APM transaction durations. -Jobs can be created per transaction type, and are based on the service's average response time. +The Machine learning integration initiates a new job predefined to calculate anomaly scores on APM transaction durations. +With this integration, you can quickly pinpoint anomalous transactions and see the health of +any upstream and downstream services. -After a machine learning job is created, results are shown in two places: +Machine learning jobs are created per environment, and are based on a service's average response time. +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. -The transaction duration graph will show the expected bounds and add an annotation when the anomaly score is 75 or above. +After a machine learning job is created, results are shown in two places: +* The transaction duration chart will show the expected bounds and add an annotation when the anomaly score is 75 or above. ++ [role="screenshot"] image::apm/images/apm-ml-integration.png[Example view of anomaly scores on response times in the APM app] -Service maps will display a color-coded anomaly indicator based on the detected anomaly score. - +* Service maps will display a color-coded anomaly indicator based on the detected anomaly score. ++ [role="screenshot"] image::apm/images/apm-service-map-anomaly.png[Example view of anomaly scores on service maps in the APM app] [float] [[create-ml-integration]] -=== Create a new machine learning job +=== Enable anomaly detection + +To enable machine learning anomaly detection: + +. From the Services overview, Traces overview, or Service Map tab, +select **Anomaly detection**. + +. Click **Create ML Job**. -To enable machine learning anomaly detection, first choose a service to monitor. -Then, select **Integrations** > **Enable ML anomaly detection** and click **Create job**. +. Machine learning jobs are created at the environment level. +Select all of the service environments that you want to enable anomaly detection in. +Anomalies will surface for all services and transaction types within the selected environments. + +. Click **Create Jobs**. That's it! After a few minutes, the job will begin calculating results; -it might take additional time for results to appear on your graph. -Jobs can be managed in *Machine Learning jobs management*. +it might take additional time for results to appear on your service maps. +Existing jobs can be managed in *Machine Learning jobs management*. APM specific anomaly detection wizards are also available for certain Agents. See the machine learning {ml-docs}/ootb-ml-jobs-apm.html[APM anomaly detection configurations] for more information. + +[float] +[[warning-ml-integration]] +=== Anomaly detection warning + +To make machine learning as easy as possible to set up, +the APM app will warn you when filtered to an environment without a machine learning job. + +[role="screenshot"] +image::apm/images/apm-anomaly-alert.png[Example view of anomaly alert in the APM app] \ No newline at end of file From 2e37239e50a5632798e17a455699e828831d37fd Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Fri, 31 Jul 2020 13:20:11 -0700 Subject: [PATCH 49/55] [Metrics UI] Fix Metrics Explorer TSVB link to use workaround pattern (#73986) * [Metrics UI] Fix Metrics Explorer TSVB link to use workaround pattern * Adding link to TSVB bug to comment --- .../helpers/create_tsvb_link.test.ts | 19 ++++++++++++++++ .../components/helpers/create_tsvb_link.ts | 22 +++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts index 04aeba41fa00d..ca4fc0abc37a4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts @@ -157,6 +157,25 @@ describe('createTSVBLink()', () => { }); }); + it('should use the workaround index pattern when there are multiple listed in the source', () => { + const customSource = { + ...source, + metricAlias: 'my-beats-*,metrics-*', + fields: { ...source.fields, timestamp: 'time' }, + }; + const link = createTSVBLink(customSource, options, series, timeRange, chartOptions); + expect(link).toStrictEqual({ + app: 'visualize', + hash: '/create', + search: { + _a: + "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metric*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metric*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))", + _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))', + type: 'metrics', + }, + }); + }); + test('createFilterFromOptions()', () => { const customOptions = { ...options, groupBy: 'host.name' }; const customSeries = { ...series, id: 'test"foo' }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index 3afc0d050e736..afddaf6621f10 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -23,6 +23,14 @@ import { SourceQuery } from '../../../../../graphql/types'; import { createMetricLabel } from './create_metric_label'; import { LinkDescriptor } from '../../../../../hooks/use_link_props'; +/* + We've recently changed the default index pattern in Metrics UI from `metricbeat-*` to + `metrics-*,metricbeat-*`. There is a bug in TSVB when there is an empty index in the pattern + the field dropdowns are not populated correctly. This index pattern is a temporary fix. + See: https://github.com/elastic/kibana/issues/73987 +*/ +const TSVB_WORKAROUND_INDEX_PATTERN = 'metric*'; + export const metricsExplorerMetricToTSVBMetric = (metric: MetricsExplorerOptionsMetric) => { if (metric.aggregation === 'rate') { const metricId = uuid.v1(); @@ -128,6 +136,13 @@ export const createFilterFromOptions = ( return { language: 'kuery', query: filters.join(' and ') }; }; +const createTSVBIndexPattern = (alias: string) => { + if (alias.split(',').length > 1) { + return TSVB_WORKAROUND_INDEX_PATTERN; + } + return alias; +}; + export const createTSVBLink = ( source: SourceQuery.Query['source']['configuration'] | undefined, options: MetricsExplorerOptions, @@ -135,6 +150,9 @@ export const createTSVBLink = ( timeRange: MetricsExplorerTimeOptions, chartOptions: MetricsExplorerChartOptions ): LinkDescriptor => { + const tsvbIndexPattern = createTSVBIndexPattern( + (source && source.metricAlias) || TSVB_WORKAROUND_INDEX_PATTERN + ); const appState = { filters: [], linked: false, @@ -147,8 +165,8 @@ export const createTSVBLink = ( axis_position: 'left', axis_scale: 'normal', id: uuid.v1(), - default_index_pattern: (source && source.metricAlias) || 'metricbeat-*', - index_pattern: (source && source.metricAlias) || 'metricbeat-*', + default_index_pattern: tsvbIndexPattern, + index_pattern: tsvbIndexPattern, interval: 'auto', series: options.metrics.map(mapMetricToSeries(chartOptions)), show_grid: 1, From 2c71a3fba9508c61995870b643c2cdb1da5d14a2 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Fri, 31 Jul 2020 22:17:24 +0100 Subject: [PATCH 50/55] [Security Solution] Fix unexpected redirect (#73969) * fix unexpected redirect * fix types Co-authored-by: Patryk Kopycinski --- .../components/open_timeline/index.test.tsx | 61 ++++++++++++++----- .../open_timeline/use_timeline_types.tsx | 26 +++++--- .../public/timelines/pages/timelines_page.tsx | 2 +- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index 6c1c88f511edb..75b6413bf08f9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -17,11 +17,25 @@ import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; import { NotePreviews } from './note_previews'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; -import { TimelineTabsStyle } from './types'; import { StatefulOpenTimeline } from '.'; + import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; + +import { useParams } from 'react-router-dom'; +import { TimelineType } from '../../../../common/types/timeline'; + jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/components/link_to'); + +jest.mock('./helpers', () => { + const originalModule = jest.requireActual('./helpers'); + return { + ...originalModule, + queryTimelineById: jest.fn(), + }; +}); + jest.mock('../../containers/all', () => { const originalModule = jest.requireActual('../../containers/all'); return { @@ -30,19 +44,21 @@ jest.mock('../../containers/all', () => { getAllTimeline: originalModule.getAllTimeline, }; }); -jest.mock('./use_timeline_types', () => { + +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { - useTimelineTypes: jest.fn().mockReturnValue({ - timelineType: 'default', - timelineTabs:
, - timelineFilters:
, - }), + ...originalModule, + useParams: jest.fn(), + useHistory: jest.fn().mockReturnValue([]), }; }); describe('StatefulOpenTimeline', () => { const title = 'All Timelines / Open Timelines'; beforeEach(() => { + (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default }); ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ fetchAllTimeline: jest.fn(), timelines: getAllTimeline( @@ -433,10 +449,7 @@ describe('StatefulOpenTimeline', () => { }); }); - /** - * enable this test when createtTemplateTimeline is ready - */ - test.skip('it renders the tabs', async () => { + test('it has the expected initial state for openTimeline - templateTimelineFilter', () => { const wrapper = mount( @@ -451,11 +464,27 @@ describe('StatefulOpenTimeline', () => { ); - await waitFor(() => { - expect( - wrapper.find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}"]`).exists() - ).toEqual(true); - }); + expect(wrapper.find('[data-test-subj="open-timeline-subtabs"]').exists()).toEqual(true); + }); + + test('it has the expected initial state for openTimelineModalBody - templateTimelineFilter', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="open-timeline-modal-body-filters"]').exists()).toEqual( + true + ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 7d54bb2209850..55afe845cdfb3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -7,26 +7,31 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui'; +import { noop } from 'lodash/fp'; import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../common/types/timeline'; import { SecurityPageName } from '../../../app/types'; import { getTimelineTabsUrl, useFormatUrl } from '../../../common/components/link_to'; import * as i18n from './translations'; import { TimelineTabsStyle, TimelineTab } from './types'; -export const useTimelineTypes = ({ - defaultTimelineCount, - templateTimelineCount, -}: { +export interface UseTimelineTypesArgs { defaultTimelineCount?: number | null; templateTimelineCount?: number | null; -}): { +} + +export interface UseTimelineTypesResult { timelineType: TimelineTypeLiteralWithNull; timelineTabs: JSX.Element; timelineFilters: JSX.Element[]; -} => { +} + +export const useTimelineTypes = ({ + defaultTimelineCount, + templateTimelineCount, +}: UseTimelineTypesArgs): UseTimelineTypesResult => { const history = useHistory(); const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines); - const { tabName } = useParams<{ pageName: string; tabName: string }>(); + const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const [timelineType, setTimelineTypes] = useState( tabName === TimelineType.default || tabName === TimelineType.template ? tabName : null ); @@ -61,7 +66,7 @@ export const useTimelineTypes = ({ timelineTabsStyle === TimelineTabsStyle.filter ? defaultTimelineCount ?? undefined : undefined, - onClick: goToTimeline, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTimeline : noop, }, { id: TimelineType.template, @@ -76,7 +81,7 @@ export const useTimelineTypes = ({ timelineTabsStyle === TimelineTabsStyle.filter ? templateTimelineCount ?? undefined : undefined, - onClick: goToTemplateTimeline, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTemplateTimeline : noop, }, ], [ @@ -106,7 +111,7 @@ export const useTimelineTypes = ({ const timelineTabs = useMemo(() => { return ( <> - + {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( { return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( { - const { tabName } = useParams(); + const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const [importDataModalToggle, setImportDataModalToggle] = useState(false); const onImportTimelineBtnClick = useCallback(() => { setImportDataModalToggle(true); From 5aca964d689f2fad5ab18c06250d6d2e8432d494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Fri, 31 Jul 2020 23:20:47 +0200 Subject: [PATCH 51/55] [Security Solution] Fix timeline pin event callback (#73981) * [Security Solution] Fix timeline pin event callback * - added tests * - restored the original disabled button behavior Co-authored-by: Andrew Goldstein --- .../components/timeline/body/helpers.test.ts | 69 +++++++++++++++++++ .../components/timeline/body/helpers.ts | 14 ++-- .../components/timeline/pin/index.test.tsx | 69 ++++++++++++++++++- .../components/timeline/pin/index.tsx | 2 +- 4 files changed, 148 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts index c8adaa891610a..f4dc691f3d059 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts @@ -9,6 +9,7 @@ import { Ecs } from '../../../../graphql/types'; import { eventHasNotes, eventIsPinned, + getPinOnClick, getPinTooltip, stringifyEvent, isInvestigateInResolverActionEnabled, @@ -298,4 +299,72 @@ describe('helpers', () => { expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); }); }); + + describe('getPinOnClick', () => { + const eventId = 'abcd'; + + test('it invokes `onPinEvent` with the expected eventId when the event is NOT pinned, and allowUnpinning is true', () => { + const isEventPinned = false; // the event is NOT pinned + const allowUnpinning = true; + const onPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent, + onUnPinEvent: jest.fn(), + isEventPinned, + }); + + expect(onPinEvent).toBeCalledWith(eventId); + }); + + test('it does NOT invoke `onPinEvent` when the event is NOT pinned, and allowUnpinning is false', () => { + const isEventPinned = false; // the event is NOT pinned + const allowUnpinning = false; + const onPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent, + onUnPinEvent: jest.fn(), + isEventPinned, + }); + + expect(onPinEvent).not.toBeCalled(); + }); + + test('it invokes `onUnPinEvent` with the expected eventId when the event is pinned, and allowUnpinning is true', () => { + const isEventPinned = true; // the event is pinned + const allowUnpinning = true; + const onUnPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent: jest.fn(), + onUnPinEvent, + isEventPinned, + }); + + expect(onUnPinEvent).toBeCalledWith(eventId); + }); + + test('it does NOT invoke `onUnPinEvent` when the event is pinned, and allowUnpinning is false', () => { + const isEventPinned = true; // the event is pinned + const allowUnpinning = false; + const onUnPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent: jest.fn(), + onUnPinEvent, + isEventPinned, + }); + + expect(onUnPinEvent).not.toBeCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts index 6a5e25632c29b..73b5a58ef7b65 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { get, isEmpty, noop } from 'lodash/fp'; + +import { get, isEmpty } from 'lodash/fp'; import { Dispatch } from 'redux'; import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; @@ -65,11 +66,16 @@ export const getPinOnClick = ({ onPinEvent, onUnPinEvent, isEventPinned, -}: GetPinOnClickParams): (() => void) => { +}: GetPinOnClickParams) => { if (!allowUnpinning) { - return noop; + return; + } + + if (isEventPinned) { + onUnPinEvent(eventId); + } else { + onPinEvent(eventId); } - return isEventPinned ? () => onUnPinEvent(eventId) : () => onPinEvent(eventId); }; /** diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx index 657976e2f4787..2ca27ded86c9d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx @@ -4,7 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getPinIcon } from './'; +import { mount } from 'enzyme'; +import React from 'react'; + +import { TimelineType } from '../../../../../common/types/timeline'; + +import { getPinIcon, Pin } from './'; + +interface ButtonIcon { + isDisabled: boolean; +} describe('pin', () => { describe('getPinRotation', () => { @@ -16,4 +25,62 @@ describe('pin', () => { expect(getPinIcon(false)).toEqual('pin'); }); }); + + describe('disabled button behavior', () => { + test('the button is enabled when allowUnpinning is true, and timelineType is NOT `template` (the default)', () => { + const allowUnpinning = true; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(false); + }); + + test('the button is disabled when allowUnpinning is false, and timelineType is NOT `template` (the default)', () => { + const allowUnpinning = false; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + + test('the button is disabled when allowUnpinning is true, and timelineType is `template`', () => { + const allowUnpinning = true; + const timelineType = TimelineType.template; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + + test('the button is disabled when allowUnpinning is false, and timelineType is `template`', () => { + const allowUnpinning = false; + const timelineType = TimelineType.template; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx index 30fe8ae0ca1f6..27780c7754d00 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx @@ -34,7 +34,7 @@ export const Pin = React.memo( iconSize={iconSize} iconType={getPinIcon(pinned)} onClick={onClick} - isDisabled={isTemplate} + isDisabled={isTemplate || !allowUnpinning} /> ); } From 75eda6690ef786f831bf7c46fc07260ffc0b37ff Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 31 Jul 2020 15:36:23 -0600 Subject: [PATCH 52/55] [SIEM] Fixes toaster errors when siemDefault index is an empty or empty spaces (#73991) ## Summary Fixes fully this issue: https://github.com/elastic/kibana/issues/49753 If you go to advanced settings and configure siemDefaultIndex to be an empty string or have empty spaces: Screen Shot 2020-07-31 at 12 52 00 PM You shouldn't get any toaster errors when going to any of the pages such as overview, detections, etc... This fixes that and adds both unit and integration tests around those areas. The fix is to add a filter which will filter all the patterns out that are either empty strings or have the _all within them rather than just looking for a single value to exist. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../graphql/source_status/resolvers.test.ts | 49 ++++++++ .../server/graphql/source_status/resolvers.ts | 31 +++-- .../lib/index_fields/elasticsearch_adapter.ts | 23 ++-- .../apis/security_solution/sources.ts | 107 +++++++++++++++--- 4 files changed, 168 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts new file mode 100644 index 0000000000000..1735c6473bb3a --- /dev/null +++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { filterIndexes } from './resolvers'; + +describe('resolvers', () => { + test('it should filter single index that has an empty string', () => { + const emptyArray = filterIndexes(['']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter single index that has blanks within it', () => { + const emptyArray = filterIndexes([' ']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter indexes that has an empty string and a valid index', () => { + const emptyArray = filterIndexes(['', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter indexes that have blanks within them and a valid index', () => { + const emptyArray = filterIndexes([' ', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter single index that has _all within it', () => { + const emptyArray = filterIndexes(['_all']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter single index that has _all within it surrounded by spaces', () => { + const emptyArray = filterIndexes([' _all ']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter indexes that _all within them and a valid index', () => { + const emptyArray = filterIndexes(['_all', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter indexes that _all surrounded with spaces within them and a valid index', () => { + const emptyArray = filterIndexes([' _all ', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); +}); diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts index 8d55e645d6791..84320b1699531 100644 --- a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts @@ -32,27 +32,34 @@ export const createSourceStatusResolvers = (libs: { }; } => ({ SourceStatus: { - async indicesExist(source, args, { req }) { - if ( - args.defaultIndex.length === 1 && - (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all') - ) { + async indicesExist(_, args, { req }) { + const indexes = filterIndexes(args.defaultIndex); + if (indexes.length !== 0) { + return libs.sourceStatus.hasIndices(req, indexes); + } else { return false; } - return libs.sourceStatus.hasIndices(req, args.defaultIndex); }, - async indexFields(source, args, { req }) { - if ( - args.defaultIndex.length === 1 && - (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all') - ) { + async indexFields(_, args, { req }) { + const indexes = filterIndexes(args.defaultIndex); + if (indexes.length !== 0) { + return libs.fields.getFields(req, indexes); + } else { return []; } - return libs.fields.getFields(req, args.defaultIndex); }, }, }); +/** + * Given a set of indexes this will remove anything that is: + * - blank or empty strings are removed as not valid indexes + * - _all is removed as that is not a valid index + * @param indexes Indexes with invalid values removed + */ +export const filterIndexes = (indexes: string[]): string[] => + indexes.filter((index) => index.trim() !== '' && index.trim() !== '_all'); + export const toIFieldSubTypeNonNullableScalar = new GraphQLScalarType({ name: 'IFieldSubType', description: 'Represents value in index pattern field item', diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts index 944fc588afc8a..bb0a4b9e2ba9b 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts @@ -17,26 +17,21 @@ import { import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { FieldsAdapter, IndexFieldDescriptor } from './types'; -type IndexesAliasIndices = Record; - export class ElasticsearchIndexFieldAdapter implements FieldsAdapter { constructor(private readonly framework: FrameworkAdapter) {} public async getIndexFields(request: FrameworkRequest, indices: string[]): Promise { const indexPatternsService = this.framework.getIndexPatternsService(request); - const indexesAliasIndices: IndexesAliasIndices = indices.reduce( - (accumulator: IndexesAliasIndices, indice: string) => { - const key = getIndexAlias(indices, indice); + const indexesAliasIndices = indices.reduce>((accumulator, indice) => { + const key = getIndexAlias(indices, indice); - if (get(key, accumulator)) { - accumulator[key] = [...accumulator[key], indice]; - } else { - accumulator[key] = [indice]; - } - return accumulator; - }, - {} as IndexesAliasIndices - ); + if (get(key, accumulator)) { + accumulator[key] = [...accumulator[key], indice]; + } else { + accumulator[key] = [indice]; + } + return accumulator; + }, {}); const responsesIndexFields: IndexFieldDescriptor[][] = await Promise.all( Object.values(indexesAliasIndices).map((indicesByGroup) => indexPatternsService.getFieldsForWildcard({ diff --git a/x-pack/test/api_integration/apis/security_solution/sources.ts b/x-pack/test/api_integration/apis/security_solution/sources.ts index a9bbf09a9e6f9..f99dd4c65fc83 100644 --- a/x-pack/test/api_integration/apis/security_solution/sources.ts +++ b/x-pack/test/api_integration/apis/security_solution/sources.ts @@ -18,22 +18,97 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('auditbeat/default')); after(() => esArchiver.unload('auditbeat/default')); - it('Make sure that we get source information when auditbeat indices is there', () => { - return client - .query({ - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - docValueFields: [], - }, - }) - .then((resp) => { - const sourceStatus = resp.data.source.status; - // test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz - expect(sourceStatus.indexFields.length).to.be(397); - expect(sourceStatus.indicesExist).to.be(true); - }); + it('Make sure that we get source information when auditbeat indices is there', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + // test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz + expect(sourceStatus.indexFields.length).to.be(397); + expect(sourceStatus.indicesExist).to.be(true); + }); + + it('should find indexes as being available when they exist', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(true); + }); + + it('should not find indexes as existing when there is an empty array of them', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there is a _all within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['_all'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there are empty strings within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [''], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there are blank spaces within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [' '], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should find indexes when one is an empty index but the others are valid', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['', 'auditbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(true); }); }); } From 6eca0b47fa1d8c3dbd540f0ded2b3ff5c4d76193 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Fri, 31 Jul 2020 14:38:39 -0700 Subject: [PATCH 53/55] Closes #73998 by using `canAccessML` in the ML capabilities API to (#73999) enable anomaly detection settings in APM. --- x-pack/plugins/apm/public/components/app/Home/index.tsx | 4 ++-- x-pack/plugins/apm/public/components/app/Settings/index.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index c6c0861c26a34..b2f15dbb11341 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -84,7 +84,7 @@ interface Props { export function Home({ tab }: Props) { const { config, core } = useApmPluginContext(); - const isMLEnabled = !!core.application.capabilities.ml; + const canAccessML = !!core.application.capabilities.ml?.canAccessML; const homeTabs = getHomeTabs(config); const selectedTab = homeTabs.find( (homeTab) => homeTab.name === tab @@ -106,7 +106,7 @@ export function Home({ tab }: Props) { - {isMLEnabled && ( + {canAccessML && ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index 1471bc345d850..cb4726244e50c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -20,7 +20,7 @@ import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; export function Settings(props: { children: ReactNode }) { const plugin = useApmPluginContext(); - const isMLEnabled = !!plugin.core.application.capabilities.ml; + const canAccessML = !!plugin.core.application.capabilities.ml?.canAccessML; const { search, pathname } = useLocation(); return ( <> @@ -51,7 +51,7 @@ export function Settings(props: { children: ReactNode }) { '/settings/agent-configuration' ), }, - ...(isMLEnabled + ...(canAccessML ? [ { name: i18n.translate( From 53b1875093a5355ba693634093b68f055dfb6f65 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 31 Jul 2020 17:50:38 -0400 Subject: [PATCH 54/55] Tweak injected metadata (#73990) Removes unnecessary fields from injected metadata for clients. --- .../rendering_service.test.ts.snap | 270 +++--------------- .../rendering/rendering_service.test.ts | 11 +- .../server/rendering/rendering_service.tsx | 5 +- src/core/server/rendering/types.ts | 7 +- 4 files changed, 55 insertions(+), 238 deletions(-) diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 95230b52c5c03..eab29731ea524 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -10,37 +10,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -83,37 +64,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -156,37 +118,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -233,37 +176,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/translations/en.json", @@ -306,37 +230,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -379,37 +284,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -452,37 +338,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/translations/en.json", @@ -525,37 +392,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -600,37 +448,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -673,37 +502,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index d1c527aca4dba..7caf4af850c10 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -30,17 +30,18 @@ const INJECTED_METADATA = { branch: expect.any(String), buildNumber: expect.any(Number), env: { - binDir: expect.any(String), - configDir: expect.any(String), - homeDir: expect.any(String), - logDir: expect.any(String), + mode: { + name: expect.any(String), + dev: expect.any(Boolean), + prod: expect.any(Boolean), + }, packageInfo: { branch: expect.any(String), buildNum: expect.any(Number), buildSha: expect.any(String), + dist: expect.any(Boolean), version: expect.any(String), }, - pluginSearchPaths: expect.any(Array), }, legacyMetadata: { branch: expect.any(String), diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 8f87d62496891..f49952ec713fb 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -55,7 +55,10 @@ export class RenderingService implements CoreService Date: Fri, 31 Jul 2020 18:39:59 -0400 Subject: [PATCH 55/55] [SECURITY_SOLUTION][ENDPOINT] Fix host list Configuration Status cell link loosing list page/size state (#73989) --- .../security_solution/public/management/common/routing.ts | 3 ++- .../public/management/pages/endpoint_hosts/view/index.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index 3636358ebe842..eeb1533f57a67 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -54,7 +54,8 @@ export const getHostListPath = ( }; export const getHostDetailsPath = ( - props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostDetailsUrlProps, + props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostIndexUIQueryParams & + HostDetailsUrlProps, search?: string ) => { const { name, ...queryParams } = props; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 58442ab417b60..f91bba3e3125a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -263,6 +263,7 @@ export const HostList = () => { render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => { const toRoutePath = getHostDetailsPath({ name: 'hostPolicyResponse', + ...queryParams, selected_host: item.metadata.host.id, }); const toRouteUrl = formatUrl(toRoutePath);