diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky
index 8121405e5ae24..370643789c2cd 100644
--- a/.ci/Jenkinsfile_flaky
+++ b/.ci/Jenkinsfile_flaky
@@ -4,7 +4,6 @@ library 'kibana-pipeline-library'
kibanaLibrary.load()
def TASK_PARAM = params.TASK ?: params.CI_GROUP
-
// Looks like 'oss:ciGroup:1', 'oss:firefoxSmoke'
def JOB_PARTS = TASK_PARAM.split(':')
def IS_XPACK = JOB_PARTS[0] == 'xpack'
@@ -111,6 +110,8 @@ def getWorkerFromParams(isXpack, job, ciGroup) {
return kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh')
} else if (job == 'apiIntegration') {
return kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh')
+ } else if (job == 'pluginFunctional') {
+ return kibanaPipeline.functionalTestProcess('oss-pluginFunctional', './test/scripts/jenkins_plugin_functional.sh')
} else {
return kibanaPipeline.ossCiGroupProcess(ciGroup)
}
diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy
index 87b64437deafc..f1095f8035b6c 100644
--- a/.ci/end2end.groovy
+++ b/.ci/end2end.groovy
@@ -13,12 +13,12 @@ pipeline {
BASE_DIR = 'src/github.com/elastic/kibana'
HOME = "${env.WORKSPACE}"
E2E_DIR = 'x-pack/plugins/apm/e2e'
- PIPELINE_LOG_LEVEL = 'DEBUG'
+ PIPELINE_LOG_LEVEL = 'INFO'
KBN_OPTIMIZER_THEMES = 'v7light'
}
options {
timeout(time: 1, unit: 'HOURS')
- buildDiscarder(logRotator(numToKeepStr: '40', artifactNumToKeepStr: '20', daysToKeepStr: '30'))
+ buildDiscarder(logRotator(numToKeepStr: '30', artifactNumToKeepStr: '10', daysToKeepStr: '30'))
timestamps()
ansiColor('xterm')
disableResume()
diff --git a/dev_docs/tutorials/building_a_plugin.mdx b/dev_docs/tutorials/building_a_plugin.mdx
index cee5a9a399de5..e751ce7d01b16 100644
--- a/dev_docs/tutorials/building_a_plugin.mdx
+++ b/dev_docs/tutorials/building_a_plugin.mdx
@@ -4,7 +4,7 @@ slug: /kibana-dev-docs/tutorials/build-a-plugin
title: Kibana plugin tutorial
summary: Anatomy of a Kibana plugin and how to build one
date: 2021-02-05
-tags: ['kibana','onboarding', 'dev', 'tutorials']
+tags: ['kibana', 'onboarding', 'dev', 'tutorials']
---
Prereading material:
@@ -14,7 +14,7 @@ Prereading material:
## The anatomy of a plugin
Plugins are defined as classes and present themselves to Kibana through a simple wrapper function. A plugin can have browser-side code, server-side code,
-or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly,
+or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly,
and you interact with Core and other plugins in the same way.
The basic file structure of a Kibana plugin named demo that has both client-side and server-side code would be:
@@ -33,7 +33,7 @@ plugins/
index.ts [6]
```
-### [1] kibana.json
+### [1] kibana.json
`kibana.json` is a static manifest file that is used to identify the plugin and to specify if this plugin has server-side code, browser-side code, or both:
@@ -42,14 +42,33 @@ plugins/
"id": "demo",
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "owner": { [1]
+ "name": "App Services",
+ "githubTeam": "kibana-app-services"
+ },
+ "description": "This plugin extends Kibana by doing xyz, and allows other plugins to extend Kibana by offering abc functionality. It also exposes some helper utilities that do efg", [2]
+ "requiredPlugins": ["data"], [3]
+ "optionalPlugins": ["alerting"] [4]
+ "requiredBundles": ["anotherPlugin"] [5]
}
```
+[1], [2]: Every internal plugin should fill in the owner and description properties.
+
+[3], [4]: Any plugin that you have a dependency on should be listed in `requiredPlugins` or `optionalPlugins`. Doing this will ensure that you have access to that plugin's start and setup contract inside your own plugin's start and setup lifecycle methods. If a plugin you optionally depend on is not installed or disabled, it will be undefined if you try to access it. If a plugin you require is not installed or disabled, kibana will fail to build.
+
+[5]: Don't worry too much about getting 5 right. The build optimizer will complain if any of these values are incorrect.
+
+
+
+ You don't need to declare a dependency on a plugin if you only wish to access its types.
+
+
### [2] public/index.ts
-`public/index.ts` is the entry point into the client-side code of this plugin. It must export a function named plugin, which will receive a standard set of
- core capabilities as an argument. It should return an instance of its plugin class for Kibana to load.
+`public/index.ts` is the entry point into the client-side code of this plugin. Everything exported from this file will be a part of the plugins . If the plugin only exists to export static utilities, consider using a package. Otherwise, this file must export a function named plugin, which will receive a standard set of
+core capabilities as an argument. It should return an instance of its plugin class for Kibana to load.
```
import type { PluginInitializerContext } from 'kibana/server';
@@ -60,13 +79,32 @@ export function plugin(initializerContext: PluginInitializerContext) {
}
```
+
+
+1. When possible, use
+
+```
+export type { AType } from '...'`
+```
+
+instead of
+
+```
+export { AType } from '...'`.
+```
+
+Using the non-`type` variation will increase the bundle size unnecessarily and may unwillingly provide access to the implementation of `AType` class.
+
+2. Don't use `export *` in these top level index.ts files
+
+
+
### [3] public/plugin.ts
`public/plugin.ts` is the client-side plugin definition itself. Technically speaking, it does not need to be a class or even a separate file from the entry
- point, but all plugins at Elastic should be consistent in this way.
+point, but all plugins at Elastic should be consistent in this way.
-
- ```ts
+```ts
import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server';
export class DemoPlugin implements Plugin {
@@ -84,10 +122,9 @@ export class DemoPlugin implements Plugin {
// called when plugin is torn down during Kibana's shutdown sequence
}
}
- ```
-
+```
-### [4] server/index.ts
+### [4] server/index.ts
`server/index.ts` is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point:
@@ -115,7 +152,7 @@ export class DemoPlugin implements Plugin {
}
```
-Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain
+Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain
considerations related to how plugins integrate with core APIs and APIs exposed by other plugins that may greatly impact how they are built.
### [6] common/index.ts
@@ -124,8 +161,8 @@ considerations related to how plugins integrate with core APIs and APIs exposed
## How plugin's interact with each other, and Core
-The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin.
-For example, the core http service exposes a function createRouter to all plugin setup functions. To use this function to register an HTTP route handler,
+The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin.
+For example, the core http service exposes a function createRouter to all plugin setup functions. To use this function to register an HTTP route handler,
a plugin just accesses it off of the first argument:
```ts
@@ -135,14 +172,16 @@ export class DemoPlugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
// handler is called when '/path' resource is requested with `GET` method
- router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' }));
+ router.get({ path: '/path', validate: false }, (context, req, res) =>
+ res.ok({ content: 'ok' })
+ );
}
}
```
Unlike core, capabilities exposed by plugins are not automatically injected into all plugins.
Instead, if a plugin wishes to use the public interface provided by another plugin, it must first declare that plugin as a
- dependency in it’s kibana.json manifest file.
+dependency in it’s kibana.json manifest file.
** foobar plugin.ts: **
@@ -174,8 +213,8 @@ export class MyPlugin implements Plugin {
}
}
```
-[1] We highly encourage plugin authors to explicitly declare public interfaces for their plugins.
+[1] We highly encourage plugin authors to explicitly declare public interfaces for their plugins.
** demo kibana.json**
@@ -194,7 +233,7 @@ With that specified in the plugin manifest, the appropriate interfaces are then
import type { CoreSetup, CoreStart } from 'kibana/server';
import type { FoobarPluginSetup, FoobarPluginStart } from '../../foobar/server';
-interface DemoSetupPlugins { [1]
+interface DemoSetupPlugins { [1]
foobar: FoobarPluginSetup;
}
@@ -218,7 +257,7 @@ export class DemoPlugin {
public stop() {}
}
```
-
+
[1] The interface for plugin’s dependencies must be manually composed. You can do this by importing the appropriate type from the plugin and constructing an interface where the property name is the plugin’s ID.
[2] These manually constructed types should then be used to specify the type of the second argument to the plugin.
diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md
index 1eaf00c7a678d..6229aeb9238e8 100644
--- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md
+++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md
@@ -16,6 +16,7 @@ Note that when generating absolute urls, the origin (protocol, host and port) ar
getUrlForApp(appId: string, options?: {
path?: string;
absolute?: boolean;
+ deepLinkId?: string;
}): string;
```
@@ -24,7 +25,7 @@ getUrlForApp(appId: string, options?: {
| Parameter | Type | Description |
| --- | --- | --- |
| appId | string
| |
-| options | {
path?: string;
absolute?: boolean;
}
| |
+| options | {
path?: string;
absolute?: boolean;
deepLinkId?: string;
}
| |
Returns:
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin._constructor_.md
similarity index 68%
rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin._constructor_.md
rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin._constructor_.md
index 64108a7c7be33..3eaf2176edf26 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin._constructor_.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin._constructor_.md
@@ -1,8 +1,8 @@
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Plugin](./kibana-plugin-plugins-data-public.plugin.md) > [(constructor)](./kibana-plugin-plugins-data-public.plugin._constructor_.md)
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md) > [(constructor)](./kibana-plugin-plugins-data-public.dataplugin._constructor_.md)
-## Plugin.(constructor)
+## DataPlugin.(constructor)
Constructs a new instance of the `DataPublicPlugin` class
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.md
new file mode 100644
index 0000000000000..4b2cad7b42882
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.md
@@ -0,0 +1,26 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md)
+
+## DataPlugin class
+
+Signature:
+
+```typescript
+export declare class DataPublicPlugin implements Plugin
+```
+
+## Constructors
+
+| Constructor | Modifiers | Description |
+| --- | --- | --- |
+| [(constructor)(initializerContext)](./kibana-plugin-plugins-data-public.dataplugin._constructor_.md) | | Constructs a new instance of the DataPublicPlugin
class |
+
+## Methods
+
+| Method | Modifiers | Description |
+| --- | --- | --- |
+| [setup(core, { bfetch, expressions, uiActions, usageCollection, inspector })](./kibana-plugin-plugins-data-public.dataplugin.setup.md) | | |
+| [start(core, { uiActions })](./kibana-plugin-plugins-data-public.dataplugin.start.md) | | |
+| [stop()](./kibana-plugin-plugins-data-public.dataplugin.stop.md) | | |
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.setup.md
similarity index 76%
rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md
rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.setup.md
index 20181a5208b52..ab1f90c1ac104 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.setup.md
@@ -1,8 +1,8 @@
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Plugin](./kibana-plugin-plugins-data-public.plugin.md) > [setup](./kibana-plugin-plugins-data-public.plugin.setup.md)
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md) > [setup](./kibana-plugin-plugins-data-public.dataplugin.setup.md)
-## Plugin.setup() method
+## DataPlugin.setup() method
Signature:
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.start.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.start.md
similarity index 70%
rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.start.md
rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.start.md
index 56934e8a29edd..4ea7ec8cd4f65 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.start.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.start.md
@@ -1,8 +1,8 @@
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Plugin](./kibana-plugin-plugins-data-public.plugin.md) > [start](./kibana-plugin-plugins-data-public.plugin.start.md)
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md) > [start](./kibana-plugin-plugins-data-public.dataplugin.start.md)
-## Plugin.start() method
+## DataPlugin.start() method
Signature:
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.stop.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.stop.md
similarity index 52%
rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.stop.md
rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.stop.md
index 8b8b63db4e03a..b7067a01b4467 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.stop.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.stop.md
@@ -1,8 +1,8 @@
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Plugin](./kibana-plugin-plugins-data-public.plugin.md) > [stop](./kibana-plugin-plugins-data-public.plugin.stop.md)
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md) > [stop](./kibana-plugin-plugins-data-public.dataplugin.stop.md)
-## Plugin.stop() method
+## DataPlugin.stop() method
Signature:
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
index 7f5a042e0ab81..7c023e756ebd5 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
@@ -11,6 +11,7 @@
| [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) | |
| [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) | |
| [AggParamType](./kibana-plugin-plugins-data-public.aggparamtype.md) | |
+| [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md) | |
| [DuplicateIndexPatternError](./kibana-plugin-plugins-data-public.duplicateindexpatternerror.md) | |
| [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) | |
| [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) | |
@@ -19,7 +20,6 @@
| [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) | |
| [OptionedParamType](./kibana-plugin-plugins-data-public.optionedparamtype.md) | |
| [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) | |
-| [Plugin](./kibana-plugin-plugins-data-public.plugin.md) | |
| [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) | |
| [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) | \* |
| [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) | Request Failure - When an entire multi request fails |
diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md
index 388f0e064d866..e51c465e912e6 100644
--- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md
+++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md
@@ -9,7 +9,7 @@ Constructs a new instance of the `AddPanelAction` class
Signature:
```typescript
-constructor(getFactory: EmbeddableStart['getEmbeddableFactory'], getAllFactories: EmbeddableStart['getEmbeddableFactories'], overlays: OverlayStart, notifications: NotificationsStart, SavedObjectFinder: React.ComponentType);
+constructor(getFactory: EmbeddableStart['getEmbeddableFactory'], getAllFactories: EmbeddableStart['getEmbeddableFactories'], overlays: OverlayStart, notifications: NotificationsStart, SavedObjectFinder: React.ComponentType, reportUiCounter?: ((appName: string, type: import("@kbn/analytics").UiCounterMetricType, eventNames: string | string[], count?: number | undefined) => void) | undefined);
```
## Parameters
@@ -21,4 +21,5 @@ constructor(getFactory: EmbeddableStart['getEmbeddableFactory'], getAllFactories
| overlays | OverlayStart
| |
| notifications | NotificationsStart
| |
| SavedObjectFinder | React.ComponentType<any>
| |
+| reportUiCounter | ((appName: string, type: import("@kbn/analytics").UiCounterMetricType, eventNames: string | string[], count?: number | undefined) => void) | undefined
| |
diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction.md
index 74a6c2b2183a2..947e506f72b43 100644
--- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction.md
+++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction.md
@@ -14,7 +14,7 @@ export declare class AddPanelAction implements Action
| Constructor | Modifiers | Description |
| --- | --- | --- |
-| [(constructor)(getFactory, getAllFactories, overlays, notifications, SavedObjectFinder)](./kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md) | | Constructs a new instance of the AddPanelAction
class |
+| [(constructor)(getFactory, getAllFactories, overlays, notifications, SavedObjectFinder, reportUiCounter)](./kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md) | | Constructs a new instance of the AddPanelAction
class |
## Properties
diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md
index 90caaa3035b34..db45b691b446e 100644
--- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md
+++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md
@@ -15,6 +15,7 @@ export declare function openAddPanelFlyout(options: {
notifications: NotificationsStart;
SavedObjectFinder: React.ComponentType;
showCreateNewMenu?: boolean;
+ reportUiCounter?: UsageCollectionStart['reportUiCounter'];
}): OverlayRef;
```
@@ -22,7 +23,7 @@ export declare function openAddPanelFlyout(options: {
| Parameter | Type | Description |
| --- | --- | --- |
-| options | {
embeddable: IContainer;
getFactory: EmbeddableStart['getEmbeddableFactory'];
getAllFactories: EmbeddableStart['getEmbeddableFactories'];
overlays: OverlayStart;
notifications: NotificationsStart;
SavedObjectFinder: React.ComponentType<any>;
showCreateNewMenu?: boolean;
}
| |
+| options | {
embeddable: IContainer;
getFactory: EmbeddableStart['getEmbeddableFactory'];
getAllFactories: EmbeddableStart['getEmbeddableFactories'];
overlays: OverlayStart;
notifications: NotificationsStart;
SavedObjectFinder: React.ComponentType<any>;
showCreateNewMenu?: boolean;
reportUiCounter?: UsageCollectionStart['reportUiCounter'];
}
| |
Returns:
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md
index c6e00842a31e6..2c03db82ba683 100644
--- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md
@@ -21,6 +21,7 @@ export interface ExpressionFunctionDefinitions
| [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md) | ExpressionFunctionDerivative
| |
| [font](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont
| |
| [moving\_average](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.moving_average.md) | ExpressionFunctionMovingAverage
| |
+| [overall\_metric](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md) | ExpressionFunctionOverallMetric
| |
| [theme](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.theme.md) | ExpressionFunctionTheme
| |
| [var\_set](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.var_set.md) | ExpressionFunctionVarSet
| |
| [var](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.var.md) | ExpressionFunctionVar
| |
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md
new file mode 100644
index 0000000000000..8685788a2f351
--- /dev/null
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md) > [overall\_metric](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md)
+
+## ExpressionFunctionDefinitions.overall\_metric property
+
+Signature:
+
+```typescript
+overall_metric: ExpressionFunctionOverallMetric;
+```
diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md
index 219678244951b..f55fed99e1d3d 100644
--- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md
+++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md
@@ -21,6 +21,7 @@ export interface ExpressionFunctionDefinitions
| [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md) | ExpressionFunctionDerivative
| |
| [font](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont
| |
| [moving\_average](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.moving_average.md) | ExpressionFunctionMovingAverage
| |
+| [overall\_metric](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md) | ExpressionFunctionOverallMetric
| |
| [theme](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.theme.md) | ExpressionFunctionTheme
| |
| [var\_set](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.var_set.md) | ExpressionFunctionVarSet
| |
| [var](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.var.md) | ExpressionFunctionVar
| |
diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md
new file mode 100644
index 0000000000000..b8564a696e6e4
--- /dev/null
+++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md) > [overall\_metric](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md)
+
+## ExpressionFunctionDefinitions.overall\_metric property
+
+Signature:
+
+```typescript
+overall_metric: ExpressionFunctionOverallMetric;
+```
diff --git a/docs/discover/search-sessions.asciidoc b/docs/discover/search-sessions.asciidoc
index fec1b8b26dd74..b503e8cfba3b4 100644
--- a/docs/discover/search-sessions.asciidoc
+++ b/docs/discover/search-sessions.asciidoc
@@ -68,3 +68,19 @@ behaves differently:
* Relative dates are converted to absolute dates.
* Panning and zooming is disabled for maps.
* Changing a filter, query, or drilldown starts a new search session, which can be slow.
+
+[float]
+==== Limitations
+
+Certain visualization features do not fully support background search sessions yet. If a dashboard using these features gets restored,
+all panels using unsupported features won't load immediately, but instead send out additional data requests which can take a while to complete.
+In this case a warning *Your search session is still running* will be shown.
+
+You can either wait for these additional requests to complete or come back to the dashboard later when all data requests have been finished.
+
+A panel on a dashboard can behave like this if one of the following features is used:
+* *Lens* - A *top values* dimension with an enabled setting *Group other values as "Other"* (configurable in the *Advanced* section of the dimension)
+* *Lens* - An *intervals* dimension is used
+* *Aggregation based* visualizations - A *terms* aggregation is used with an enabled setting *Group other values in separate bucket*
+* *Aggregation based* visualizations - A *histogram* aggregation is used
+* *Maps* - Layers using joins, blended layers or tracks layers are used
diff --git a/docs/user/alerting/domain-specific-rules.asciidoc b/docs/user/alerting/domain-specific-rules.asciidoc
deleted file mode 100644
index f509f9e528823..0000000000000
--- a/docs/user/alerting/domain-specific-rules.asciidoc
+++ /dev/null
@@ -1,20 +0,0 @@
-[role="xpack"]
-[[domain-specific-rules]]
-== Domain-specific rules
-
-For domain-specific rules, refer to the documentation for that app.
-{kib} supports these rules:
-
-* {observability-guide}/create-alerts.html[Observability rules]
-* {security-guide}/prebuilt-rules.html[Security rules]
-* <>
-* {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[]
-
-[NOTE]
-==============================================
-Some rule types are subscription features, while others are free features.
-For a comparison of the Elastic subscription levels,
-see {subscriptions}[the subscription page].
-==============================================
-
-include::map-rules/geo-rule-types.asciidoc[]
diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc
index 68cf3ee070b08..9ab6a2dc46ebf 100644
--- a/docs/user/alerting/index.asciidoc
+++ b/docs/user/alerting/index.asciidoc
@@ -3,6 +3,5 @@ include::alerting-setup.asciidoc[]
include::create-and-manage-rules.asciidoc[]
include::defining-rules.asciidoc[]
include::rule-management.asciidoc[]
-include::stack-rules.asciidoc[]
-include::domain-specific-rules.asciidoc[]
+include::rule-types.asciidoc[]
include::alerting-troubleshooting.asciidoc[]
diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc
new file mode 100644
index 0000000000000..bb840014fe80f
--- /dev/null
+++ b/docs/user/alerting/rule-types.asciidoc
@@ -0,0 +1,56 @@
+[role="xpack"]
+[[rule-types]]
+== Rule types
+
+A rule is a set of <>, <>, and <> that enable notifications. {kib} provides two types of rules: rules specific to the Elastic Stack and rules specific to a domain.
+
+[NOTE]
+==============================================
+Some rule types are subscription features, while others are free features.
+For a comparison of the Elastic subscription levels,
+see {subscriptions}[the subscription page].
+==============================================
+
+[float]
+[[stack-rules]]
+=== Stack rules
+
+<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information.
+
+[cols="2*<"]
+|===
+
+| <>
+| Aggregate field values from documents using {es} queries, compare them to threshold values, and schedule actions to run when the thresholds are met.
+
+| <>
+| Run a user-configured {es} query, compare the number of matches to a configured threshold, and schedule actions to run when the threshold condition is met.
+
+|===
+
+[float]
+[[domain-specific-rules]]
+=== Domain rules
+
+Domain rules are registered by *Observability*, *Security*, <> and <>.
+
+[cols="2*<"]
+|===
+
+| {observability-guide}/create-alerts.html[Observability rules]
+| Detect complex conditions in the *Logs*, *Metrics*, and *Uptime* apps.
+
+| {security-guide}/prebuilt-rules.html[Security rules]
+| Detect suspicous source events with pre-built or custom rules and create alerts when a rule’s conditions are met.
+
+| <>
+| Run an {es} query to determine if any documents are currently contained in any boundaries from a specified boundary index and generate alerts when a rule's conditions are met.
+
+| {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[]
+| Run scheduled checks on an anomaly detection job to detect anomalies with certain conditions. If an anomaly meets the conditions, an alert is created and the associated action is triggered.
+
+|===
+
+include::rule-types/index-threshold.asciidoc[]
+include::rule-types/es-query.asciidoc[]
+include::rule-types/geo-rule-types.asciidoc[]
diff --git a/docs/user/alerting/stack-rules/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc
similarity index 100%
rename from docs/user/alerting/stack-rules/es-query.asciidoc
rename to docs/user/alerting/rule-types/es-query.asciidoc
diff --git a/docs/user/alerting/map-rules/geo-rule-types.asciidoc b/docs/user/alerting/rule-types/geo-rule-types.asciidoc
similarity index 74%
rename from docs/user/alerting/map-rules/geo-rule-types.asciidoc
rename to docs/user/alerting/rule-types/geo-rule-types.asciidoc
index eee7b59252205..244cf90c855a7 100644
--- a/docs/user/alerting/map-rules/geo-rule-types.asciidoc
+++ b/docs/user/alerting/rule-types/geo-rule-types.asciidoc
@@ -1,16 +1,14 @@
[role="xpack"]
[[geo-alerting]]
-=== Geo rule type
+=== Tracking containment
-Alerting now includes one additional stack rule: <>.
-
-As with other stack rules, you need `all` access to the *Stack Rules* feature
-to be able to create and edit a geo rule.
-See <> for more information on configuring roles that provide access to this feature.
+<> offers the Tracking containment rule type which runs an {es} query over indices to determine whether any
+documents are currently contained within any boundaries from the specified boundary index.
+In the event that an entity is contained within a boundary, an alert may be generated.
[float]
-==== Geo alerting requirements
-To create a *Tracking containment* rule, the following requirements must be present:
+==== Requirements
+To create a Tracking containment rule, the following requirements must be present:
- *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field,
and some form of entity identifier. An entity identifier is a `keyword` or `number`
@@ -29,22 +27,12 @@ than the current time minus the amount of the interval. If data older than
`now - ` is ingested, it won't trigger a rule.
[float]
-==== Creating a geo rule
-Click the *Create* button in the <>.
-Complete the <>.
-
-[role="screenshot"]
-image::user/alerting/images/alert-types-tracking-select.png[Choosing a tracking rule type]
+==== Create the rule
-[float]
-[[rule-type-tracking-containment]]
-==== Tracking containment
-The Tracking containment rule type runs an {es} query over indices, determining if any
-documents are currently contained within any boundaries from the specified boundary index.
-In the event that an entity is contained within a boundary, an alert may be generated.
+Fill in the <>, then select Tracking containment.
[float]
-===== Defining the conditions
+==== Define the conditions
Tracking containment rules have 3 clauses that define the condition to detect,
as well as 2 Kuery bars used to provide additional filtering context for each of the indices.
@@ -61,6 +49,9 @@ Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_sha
identifying boundaries, and an optional *Human-readable boundary name* for better alerting
messages.
+[float]
+==== Add action
+
Conditions for how a rule is tracked can be specified uniquely for each individual action.
A rule can be triggered either when a containment condition is met or when an entity
is no longer contained.
diff --git a/docs/user/alerting/stack-rules/index-threshold.asciidoc b/docs/user/alerting/rule-types/index-threshold.asciidoc
similarity index 100%
rename from docs/user/alerting/stack-rules/index-threshold.asciidoc
rename to docs/user/alerting/rule-types/index-threshold.asciidoc
diff --git a/docs/user/alerting/stack-rules.asciidoc b/docs/user/alerting/stack-rules.asciidoc
deleted file mode 100644
index 483834c78806e..0000000000000
--- a/docs/user/alerting/stack-rules.asciidoc
+++ /dev/null
@@ -1,27 +0,0 @@
-[role="xpack"]
-[[stack-rules]]
-== Stack rule types
-
-Kibana provides two types of rules:
-
-* Stack rules, which are built into {kib}
-* <>, which are registered by {kib} apps.
-
-{kib} provides two stack rules:
-
-* <>
-* <>
-
-Users require the `all` privilege to access the *Stack Rules* feature and create and edit rules.
-See <> for more information.
-
-[NOTE]
-==============================================
-Some rule types are subscription features, while others are free features.
-For a comparison of the Elastic subscription levels,
-see {subscriptions}[the subscription page].
-==============================================
-
-
-include::stack-rules/index-threshold.asciidoc[]
-include::stack-rules/es-query.asciidoc[]
diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc
index 39e596df4af34..001114578a1cd 100644
--- a/docs/user/dashboard/aggregation-reference.asciidoc
+++ b/docs/user/dashboard/aggregation-reference.asciidoc
@@ -23,7 +23,7 @@ This reference can help simplify the comparison if you need a specific feature.
| Table with summary row
^| X
-|
+^| X
|
|
|
@@ -65,7 +65,7 @@ This reference can help simplify the comparison if you need a specific feature.
| Heat map
^| X
-|
+^| X
|
|
^| X
@@ -333,7 +333,7 @@ build their advanced visualization.
| Math on aggregated data
|
-|
+^| X
^| X
^| X
^| X
@@ -352,6 +352,13 @@ build their advanced visualization.
^| X
^| X
+| Time shifts
+|
+^| X
+^| X
+^| X
+^| X
+
| Fully custom {es} queries
|
|
diff --git a/docs/user/dashboard/create-panels-with-editors.asciidoc b/docs/user/dashboard/create-panels-with-editors.asciidoc
index 17d3b5fb8a8a5..77a4706e249fd 100644
--- a/docs/user/dashboard/create-panels-with-editors.asciidoc
+++ b/docs/user/dashboard/create-panels-with-editors.asciidoc
@@ -30,13 +30,16 @@
[[lens-editor]]
=== Lens
-*Lens* is the drag and drop editor that creates visualizations of your data.
+*Lens* is the drag and drop editor that creates visualizations of your data, recommended for most
+users.
With *Lens*, you can:
* Use the automatically generated suggestions to change the visualization type.
* Create visualizations with multiple layers and indices.
* Change the aggregation and labels to customize the data.
+* Perform math on aggregations using *Formula*.
+* Use time shifts to compare data at two times, such as month over month.
[role="screenshot"]
image:dashboard/images/lens_advanced_1_1.png[Lens]
diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc
index 9f17a380bc209..7927489c596d7 100644
--- a/docs/user/dashboard/lens.asciidoc
+++ b/docs/user/dashboard/lens.asciidoc
@@ -300,7 +300,9 @@ image::images/lens_missing_values_strategy.png[Lens Missing values strategies me
[[is-it-possible-to-change-the-scale-of-Y-axis]]
===== Is it possible to statically define the scale of the y-axis in a visualization?
-The ability to start the y-axis from another value than 0, or use a logarithmic scale, is unsupported in *Lens*.
+Yes, you can set the bounds on bar, line and area chart types in Lens, unless using percentage mode. Bar
+and area charts must have 0 in the bounds. Logarithmic scales are unsupported in *Lens*.
+To set the y-axis bounds, click the icon representing the axis you want to customize.
[float]
[[is-it-possible-to-have-pagination-for-datatable]]
diff --git a/docs/user/production-considerations/production.asciidoc b/docs/user/production-considerations/production.asciidoc
index 1ffca4b6ae6ab..b75b556588cfd 100644
--- a/docs/user/production-considerations/production.asciidoc
+++ b/docs/user/production-considerations/production.asciidoc
@@ -122,8 +122,6 @@ active in case of failure from the currently used instance.
Kibana can be configured to connect to multiple Elasticsearch nodes in the same cluster. In situations where a node becomes unavailable,
Kibana will transparently connect to an available node and continue operating. Requests to available hosts will be routed in a round robin fashion.
-Currently the Console application is limited to connecting to the first node listed.
-
In kibana.yml:
[source,js]
--------
diff --git a/package.json b/package.json
index b4f9109503261..c9c6fa7f582c5 100644
--- a/package.json
+++ b/package.json
@@ -99,7 +99,7 @@
"dependencies": {
"@elastic/apm-rum": "^5.6.1",
"@elastic/apm-rum-react": "^1.2.5",
- "@elastic/charts": "30.0.0",
+ "@elastic/charts": "30.1.0",
"@elastic/datemath": "link:bazel-bin/packages/elastic-datemath",
"@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13",
"@elastic/ems-client": "7.13.0",
@@ -215,7 +215,6 @@
"cytoscape-dagre": "^2.2.2",
"d3": "3.5.17",
"d3-array": "1.2.4",
- "d3-cloud": "1.2.5",
"d3-scale": "1.0.7",
"d3-shape": "^1.1.0",
"d3-time": "^1.1.0",
@@ -671,7 +670,7 @@
"callsites": "^3.1.0",
"chai": "3.5.0",
"chance": "1.0.18",
- "chromedriver": "^90.0.0",
+ "chromedriver": "^91.0.1",
"clean-webpack-plugin": "^3.0.0",
"cmd-shim": "^2.1.0",
"compression-webpack-plugin": "^4.0.0",
@@ -839,4 +838,4 @@
"yargs": "^15.4.1",
"zlib": "^1.0.5"
}
-}
+}
\ No newline at end of file
diff --git a/packages/kbn-apm-utils/src/index.ts b/packages/kbn-apm-utils/src/index.ts
index 384b6683199e5..09a6989091f60 100644
--- a/packages/kbn-apm-utils/src/index.ts
+++ b/packages/kbn-apm-utils/src/index.ts
@@ -14,6 +14,7 @@ export interface SpanOptions {
type?: string;
subtype?: string;
labels?: Record;
+ intercept?: boolean;
}
type Span = Exclude;
@@ -36,23 +37,27 @@ export async function withSpan(
): Promise {
const options = parseSpanOptions(optionsOrName);
- const { name, type, subtype, labels } = options;
+ const { name, type, subtype, labels, intercept } = options;
if (!agent.isStarted()) {
return cb();
}
+ let createdSpan: Span | undefined;
+
// When a span starts, it's marked as the active span in its context.
// When it ends, it's not untracked, which means that if a span
// starts directly after this one ends, the newly started span is a
// child of this span, even though it should be a sibling.
// To mitigate this, we queue a microtask by awaiting a promise.
- await Promise.resolve();
+ if (!intercept) {
+ await Promise.resolve();
- const span = agent.startSpan(name);
+ createdSpan = agent.startSpan(name) ?? undefined;
- if (!span) {
- return cb();
+ if (!createdSpan) {
+ return cb();
+ }
}
// If a span is created in the same context as the span that we just
@@ -61,33 +66,51 @@ export async function withSpan(
// mitigate this we create a new context.
return runInNewContext(() => {
+ const promise = cb(createdSpan);
+
+ let span: Span | undefined = createdSpan;
+
+ if (intercept) {
+ span = agent.currentSpan ?? undefined;
+ }
+
+ if (!span) {
+ return promise;
+ }
+
+ const targetedSpan = span;
+
+ if (name) {
+ targetedSpan.name = name;
+ }
+
// @ts-ignore
if (type) {
- span.type = type;
+ targetedSpan.type = type;
}
if (subtype) {
- span.subtype = subtype;
+ targetedSpan.subtype = subtype;
}
if (labels) {
- span.addLabels(labels);
+ targetedSpan.addLabels(labels);
}
- return cb(span)
+ return promise
.then((res) => {
- if (!span.outcome || span.outcome === 'unknown') {
- span.outcome = 'success';
+ if (!targetedSpan.outcome || targetedSpan.outcome === 'unknown') {
+ targetedSpan.outcome = 'success';
}
return res;
})
.catch((err) => {
- if (!span.outcome || span.outcome === 'unknown') {
- span.outcome = 'failure';
+ if (!targetedSpan.outcome || targetedSpan.outcome === 'unknown') {
+ targetedSpan.outcome = 'failure';
}
throw err;
})
.finally(() => {
- span.end();
+ targetedSpan.end();
});
});
}
diff --git a/packages/kbn-test/src/jest/utils/router_helpers.tsx b/packages/kbn-test/src/jest/utils/router_helpers.tsx
index e2245440274d1..85ef27488a4ce 100644
--- a/packages/kbn-test/src/jest/utils/router_helpers.tsx
+++ b/packages/kbn-test/src/jest/utils/router_helpers.tsx
@@ -8,18 +8,39 @@
import React, { Component, ComponentType } from 'react';
import { MemoryRouter, Route, withRouter } from 'react-router-dom';
-import * as H from 'history';
+import { History, LocationDescriptor } from 'history';
-export const WithMemoryRouter = (initialEntries: string[] = ['/'], initialIndex: number = 0) => (
- WrappedComponent: ComponentType
-) => (props: any) => (
+const stringifyPath = (path: LocationDescriptor): string => {
+ if (typeof path === 'string') {
+ return path;
+ }
+
+ return path.pathname || '/';
+};
+
+const locationDescriptorToRoutePath = (
+ paths: LocationDescriptor | LocationDescriptor[]
+): string | string[] => {
+ if (Array.isArray(paths)) {
+ return paths.map((path: LocationDescriptor) => {
+ return stringifyPath(path);
+ });
+ }
+
+ return stringifyPath(paths);
+};
+
+export const WithMemoryRouter = (
+ initialEntries: LocationDescriptor[] = ['/'],
+ initialIndex: number = 0
+) => (WrappedComponent: ComponentType) => (props: any) => (
);
export const WithRoute = (
- componentRoutePath: string | string[] = '/',
+ componentRoutePath: LocationDescriptor | LocationDescriptor[] = ['/'],
onRouter = (router: any) => {}
) => (WrappedComponent: ComponentType) => {
// Create a class component that will catch the router
@@ -40,16 +61,16 @@ export const WithRoute = (
return (props: any) => (
}
/>
);
};
interface Router {
- history: Partial;
+ history: Partial;
route: {
- location: H.Location;
+ location: LocationDescriptor;
};
}
diff --git a/packages/kbn-test/src/jest/utils/testbed/types.ts b/packages/kbn-test/src/jest/utils/testbed/types.ts
index fdc000215c4f1..bba504951c0bc 100644
--- a/packages/kbn-test/src/jest/utils/testbed/types.ts
+++ b/packages/kbn-test/src/jest/utils/testbed/types.ts
@@ -8,6 +8,7 @@
import { Store } from 'redux';
import { ReactWrapper } from 'enzyme';
+import { LocationDescriptor } from 'history';
export type SetupFunc = (props?: any) => TestBed | Promise>;
@@ -161,11 +162,11 @@ export interface MemoryRouterConfig {
/** Flag to add or not the `MemoryRouter`. If set to `false`, there won't be any router and the component won't be wrapped on a ` `. */
wrapComponent?: boolean;
/** The React Router **initial entries** setting ([see documentation](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md)) */
- initialEntries?: string[];
+ initialEntries?: LocationDescriptor[];
/** The React Router **initial index** setting ([see documentation](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md)) */
initialIndex?: number;
/** The route **path** for the mounted component (defaults to `"/"`) */
- componentRoutePath?: string | string[];
+ componentRoutePath?: LocationDescriptor | LocationDescriptor[];
/** A callBack that will be called with the React Router instance once mounted */
onRouter?: (router: any) => void;
}
diff --git a/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts b/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts
new file mode 100644
index 0000000000000..25651a0dd2190
--- /dev/null
+++ b/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { parseArchive } from './parse_archive';
+
+jest.mock('fs/promises', () => ({
+ readFile: jest.fn(),
+}));
+
+const mockReadFile = jest.requireMock('fs/promises').readFile;
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it('parses archives with \\n', async () => {
+ mockReadFile.mockResolvedValue(
+ `{
+ "foo": "abc"
+ }\n\n{
+ "foo": "xyz"
+ }`
+ );
+
+ const archive = await parseArchive('mock');
+ expect(archive).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "foo": "abc",
+ },
+ Object {
+ "foo": "xyz",
+ },
+ ]
+ `);
+});
+
+it('parses archives with \\r\\n', async () => {
+ mockReadFile.mockResolvedValue(
+ `{
+ "foo": "123"
+ }\r\n\r\n{
+ "foo": "456"
+ }`
+ );
+
+ const archive = await parseArchive('mock');
+ expect(archive).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "foo": "123",
+ },
+ Object {
+ "foo": "456",
+ },
+ ]
+ `);
+});
diff --git a/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts b/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts
new file mode 100644
index 0000000000000..b6b85ba521525
--- /dev/null
+++ b/packages/kbn-test/src/kbn_client/import_export/parse_archive.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import Fs from 'fs/promises';
+
+export interface SavedObject {
+ id: string;
+ type: string;
+ [key: string]: unknown;
+}
+
+export async function parseArchive(path: string): Promise {
+ return (await Fs.readFile(path, 'utf-8'))
+ .split(/\r?\n\r?\n/)
+ .filter((line) => !!line)
+ .map((line) => JSON.parse(line));
+}
diff --git a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts
index 88953cdbaed7c..4adae7d1cd031 100644
--- a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts
+++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts
@@ -16,25 +16,12 @@ import { ToolingLog, isAxiosResponseError, createFailError, REPO_ROOT } from '@k
import { KbnClientRequester, uriencode, ReqOptions } from './kbn_client_requester';
import { KbnClientSavedObjects } from './kbn_client_saved_objects';
+import { parseArchive } from './import_export/parse_archive';
interface ImportApiResponse {
success: boolean;
[key: string]: unknown;
}
-
-interface SavedObject {
- id: string;
- type: string;
- [key: string]: unknown;
-}
-
-async function parseArchive(path: string): Promise {
- return (await Fs.readFile(path, 'utf-8'))
- .split('\n\n')
- .filter((line) => !!line)
- .map((line) => JSON.parse(line));
-}
-
export class KbnClientImportExport {
constructor(
public readonly log: ToolingLog,
diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts
index 5658d3f626077..3ed164088bf5c 100644
--- a/src/core/public/application/application_service.test.ts
+++ b/src/core/public/application/application_service.test.ts
@@ -497,6 +497,56 @@ describe('#start()', () => {
expect(getUrlForApp('app1', { path: 'deep/link///' })).toBe('/base-path/app/app1/deep/link');
});
+ describe('deepLinkId option', () => {
+ it('ignores the deepLinkId parameter if it is unknown', async () => {
+ service.setup(setupDeps);
+
+ service.setup(setupDeps);
+ const { getUrlForApp } = await service.start(startDeps);
+
+ expect(getUrlForApp('app1', { deepLinkId: 'unkown-deep-link' })).toBe(
+ '/base-path/app/app1'
+ );
+ });
+
+ it('creates URLs with deepLinkId parameter', async () => {
+ const { register } = service.setup(setupDeps);
+
+ register(
+ Symbol(),
+ createApp({
+ id: 'app1',
+ appRoute: '/custom/app-path',
+ deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }],
+ })
+ );
+
+ const { getUrlForApp } = await service.start(startDeps);
+
+ expect(getUrlForApp('app1', { deepLinkId: 'dl1' })).toBe(
+ '/base-path/custom/app-path/deep-link'
+ );
+ });
+
+ it('creates URLs with deepLinkId and path parameters', async () => {
+ const { register } = service.setup(setupDeps);
+
+ register(
+ Symbol(),
+ createApp({
+ id: 'app1',
+ appRoute: '/custom/app-path',
+ deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }],
+ })
+ );
+
+ const { getUrlForApp } = await service.start(startDeps);
+ expect(getUrlForApp('app1', { deepLinkId: 'dl1', path: 'foo/bar' })).toBe(
+ '/base-path/custom/app-path/deep-link/foo/bar'
+ );
+ });
+ });
+
it('does not append trailing slash if hash is provided in path parameter', async () => {
service.setup(setupDeps);
const { getUrlForApp } = await service.start(startDeps);
diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx
index 32d45b32c32ff..8c6090caabce1 100644
--- a/src/core/public/application/application_service.tsx
+++ b/src/core/public/application/application_service.tsx
@@ -282,8 +282,19 @@ export class ApplicationService {
history: this.history!,
getUrlForApp: (
appId,
- { path, absolute = false }: { path?: string; absolute?: boolean } = {}
+ {
+ path,
+ absolute = false,
+ deepLinkId,
+ }: { path?: string; absolute?: boolean; deepLinkId?: string } = {}
) => {
+ if (deepLinkId) {
+ const deepLinkPath = getAppDeepLinkPath(availableMounters, appId, deepLinkId);
+ if (deepLinkPath) {
+ path = appendAppPath(deepLinkPath, path);
+ }
+ }
+
const relUrl = http.basePath.prepend(getAppUrl(availableMounters, appId, path));
return absolute ? relativeToAbsolute(relUrl) : relUrl;
},
diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts
index 60b0dbf158dd9..5803f2e3779ab 100644
--- a/src/core/public/application/types.ts
+++ b/src/core/public/application/types.ts
@@ -780,7 +780,10 @@ export interface ApplicationStart {
* @param options.path - optional path inside application to deep link to
* @param options.absolute - if true, will returns an absolute url instead of a relative one
*/
- getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean }): string;
+ getUrlForApp(
+ appId: string,
+ options?: { path?: string; absolute?: boolean; deepLinkId?: string }
+ ): string;
/**
* An observable that emits the current application id and each subsequent id update.
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 53428edf4b345..06277d9351922 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -142,7 +142,7 @@ export class DocLinksService {
dataStreams: `${ELASTICSEARCH_DOCS}data-streams.html`,
indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`,
indexSettings: `${ELASTICSEARCH_DOCS}index-modules.html#index-modules-settings`,
- indexTemplates: `${ELASTICSEARCH_DOCS}indices-templates.html`,
+ indexTemplates: `${ELASTICSEARCH_DOCS}index-templates.html`,
mapping: `${ELASTICSEARCH_DOCS}mapping.html`,
mappingAnalyzer: `${ELASTICSEARCH_DOCS}analyzer.html`,
mappingCoerce: `${ELASTICSEARCH_DOCS}coerce.html`,
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 235110aeb4633..d3426b50f7614 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -150,6 +150,7 @@ export interface ApplicationStart {
getUrlForApp(appId: string, options?: {
path?: string;
absolute?: boolean;
+ deepLinkId?: string;
}): string;
navigateToApp(appId: string, options?: NavigateToAppOptions): Promise;
navigateToUrl(url: string): Promise;
diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_oss_sample_saved_objects.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_oss_sample_saved_objects.zip
deleted file mode 100644
index abb8dd2b6d491..0000000000000
Binary files a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_oss_sample_saved_objects.zip and /dev/null differ
diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip
new file mode 100644
index 0000000000000..ff02fcf204845
Binary files /dev/null and b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip differ
diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts
index f9d8e7cc4fbaa..f4e0dd8fffcab 100644
--- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts
+++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts
@@ -21,13 +21,37 @@ import { Root } from '../../../root';
const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version;
-const logFilePath = Path.join(__dirname, 'migration_test_kibana.log');
+const logFilePath = Path.join(__dirname, 'migration_test_kibana_from_v1.log');
const asyncUnlink = Util.promisify(Fs.unlink);
async function removeLogFile() {
// ignore errors if it doesn't exist
await asyncUnlink(logFilePath).catch(() => void 0);
}
+const assertMigratedDocuments = (arr: any[], target: any[]) => target.every((v) => arr.includes(v));
+
+function sortByTypeAndId(a: { type: string; id: string }, b: { type: string; id: string }) {
+ return a.type.localeCompare(b.type) || a.id.localeCompare(b.id);
+}
+
+async function fetchDocuments(esClient: ElasticsearchClient, index: string) {
+ const { body } = await esClient.search({
+ index,
+ body: {
+ query: {
+ match_all: {},
+ },
+ _source: ['type', 'id'],
+ },
+ });
+
+ return body.hits.hits
+ .map((h) => ({
+ ...h._source,
+ id: h._id,
+ }))
+ .sort(sortByTypeAndId);
+}
describe('migration v2', () => {
let esServer: kbnTestServer.TestElasticsearchUtils;
@@ -40,7 +64,7 @@ describe('migration v2', () => {
adjustTimeout: (t: number) => jest.setTimeout(t),
settings: {
es: {
- license: 'trial',
+ license: 'basic',
dataArchive,
},
},
@@ -51,8 +75,8 @@ describe('migration v2', () => {
migrations: {
skip: false,
enableV2: true,
- // There are 53 docs in fixtures. Batch size configured to enforce 3 migration steps.
- batchSize: 20,
+ // There are 40 docs in fixtures. Batch size configured to enforce 3 migration steps.
+ batchSize: 15,
},
logging: {
appenders: {
@@ -85,8 +109,7 @@ describe('migration v2', () => {
coreStart = start;
esClient = coreStart.elasticsearch.client.asInternalUser;
});
-
- await Promise.all([startEsPromise, startKibanaPromise]);
+ return await Promise.all([startEsPromise, startKibanaPromise]);
};
const getExpectedVersionPerType = () =>
@@ -192,15 +215,19 @@ describe('migration v2', () => {
});
});
- // FLAKY: https://github.com/elastic/kibana/issues/91107
- describe.skip('migrating from the same Kibana version', () => {
+ describe('migrating from the same Kibana version that used v1 migrations', () => {
+ const originalIndex = `.kibana_1`; // v1 migrations index
const migratedIndex = `.kibana_${kibanaVersion}_001`;
beforeAll(async () => {
await removeLogFile();
await startServers({
- oss: true,
- dataArchive: Path.join(__dirname, 'archives', '8.0.0_oss_sample_saved_objects.zip'),
+ oss: false,
+ dataArchive: Path.join(
+ __dirname,
+ 'archives',
+ '8.0.0_v1_migrations_sample_data_saved_objects.zip'
+ ),
});
});
@@ -215,7 +242,6 @@ describe('migration v2', () => {
},
{ ignore: [404] }
);
-
const response = body[migratedIndex];
expect(response).toBeDefined();
@@ -225,17 +251,23 @@ describe('migration v2', () => {
]);
});
- it('copies all the document of the previous index to the new one', async () => {
+ it('copies the documents from the previous index to the new one', async () => {
+ // original assertion on document count comparison (how atteched are we to this assertion?)
const migratedIndexResponse = await esClient.count({
index: migratedIndex,
});
const oldIndexResponse = await esClient.count({
- index: '.kibana_1',
+ index: originalIndex,
});
// Use a >= comparison since once Kibana has started it might create new
// documents like telemetry tasks
expect(migratedIndexResponse.body.count).toBeGreaterThanOrEqual(oldIndexResponse.body.count);
+
+ // new assertion against a document array comparison
+ const originalDocs = await fetchDocuments(esClient, originalIndex);
+ const migratedDocs = await fetchDocuments(esClient, migratedIndex);
+ expect(assertMigratedDocuments(migratedDocs, originalDocs));
});
it('migrates the documents to the highest version', async () => {
diff --git a/src/dev/typescript/build_ts_refs.ts b/src/dev/typescript/build_ts_refs.ts
index 2e25827996e45..26425b7a3e61d 100644
--- a/src/dev/typescript/build_ts_refs.ts
+++ b/src/dev/typescript/build_ts_refs.ts
@@ -13,12 +13,20 @@ import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils';
export const REF_CONFIG_PATHS = [Path.resolve(REPO_ROOT, 'tsconfig.refs.json')];
-export async function buildAllTsRefs(log: ToolingLog) {
+export async function buildAllTsRefs(log: ToolingLog): Promise<{ failed: boolean }> {
for (const path of REF_CONFIG_PATHS) {
const relative = Path.relative(REPO_ROOT, path);
log.debug(`Building TypeScript projects refs for ${relative}...`);
- await execa(require.resolve('typescript/bin/tsc'), ['-b', relative, '--pretty'], {
- cwd: REPO_ROOT,
- });
+ const { failed, stdout } = await execa(
+ require.resolve('typescript/bin/tsc'),
+ ['-b', relative, '--pretty'],
+ {
+ cwd: REPO_ROOT,
+ reject: false,
+ }
+ );
+ log.info(stdout);
+ if (failed) return { failed };
}
+ return { failed: false };
}
diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts
index f95c230f44b9e..d9e9eb036fe0f 100644
--- a/src/dev/typescript/run_type_check_cli.ts
+++ b/src/dev/typescript/run_type_check_cli.ts
@@ -69,7 +69,11 @@ export async function runTypeCheckCli() {
process.exit();
}
- await buildAllTsRefs(log);
+ const { failed } = await buildAllTsRefs(log);
+ if (failed) {
+ log.error('Unable to build TS project refs');
+ process.exit(1);
+ }
const tscArgs = [
// composite project cannot be used with --noEmit
diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx
index 1cfa39d5e0e79..e5f89bd6a8e90 100644
--- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx
+++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx
@@ -132,7 +132,7 @@ export function DashboardTopNav({
const trackUiMetric = usageCollection?.reportUiCounter.bind(
usageCollection,
- DashboardConstants.DASHBOARDS_ID
+ DashboardConstants.DASHBOARD_ID
);
useEffect(() => {
@@ -163,6 +163,7 @@ export function DashboardTopNav({
notifications: core.notifications,
overlays: core.overlays,
SavedObjectFinder: getSavedObjectFinder(core.savedObjects, uiSettings),
+ reportUiCounter: usageCollection?.reportUiCounter,
}),
}));
}
@@ -174,6 +175,7 @@ export function DashboardTopNav({
core.savedObjects,
core.overlays,
uiSettings,
+ usageCollection,
]);
const createNewVisType = useCallback(
@@ -183,7 +185,7 @@ export function DashboardTopNav({
if (visType) {
if (trackUiMetric) {
- trackUiMetric(METRIC_TYPE.CLICK, visType.name);
+ trackUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`);
}
if ('aliasPath' in visType) {
diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx
index 90cf0fcd571a1..74d725bb4d104 100644
--- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx
+++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx
@@ -51,7 +51,7 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => {
const trackUiMetric = usageCollection?.reportUiCounter.bind(
usageCollection,
- DashboardConstants.DASHBOARDS_ID
+ DashboardConstants.DASHBOARD_ID
);
const createNewAggsBasedVis = useCallback(
diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
index 2aa0d346afe34..523bbe1f01018 100644
--- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
+++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
@@ -174,6 +174,57 @@ const nestedTermResponse = {
status: 200,
};
+const exhaustiveNestedTermResponse = {
+ took: 10,
+ timed_out: false,
+ _shards: {
+ total: 1,
+ successful: 1,
+ skipped: 0,
+ failed: 0,
+ },
+ hits: {
+ total: 14005,
+ max_score: 0,
+ hits: [],
+ },
+ aggregations: {
+ '1': {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 8325,
+ buckets: [
+ {
+ '2': {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 0,
+ buckets: [
+ { key: 'ios', doc_count: 2850 },
+ { key: 'win xp', doc_count: 2830 },
+ { key: '__missing__', doc_count: 1430 },
+ ],
+ },
+ key: 'US-with-dash',
+ doc_count: 2850,
+ },
+ {
+ '2': {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 0,
+ buckets: [
+ { key: 'ios', doc_count: 1850 },
+ { key: 'win xp', doc_count: 1830 },
+ { key: '__missing__', doc_count: 130 },
+ ],
+ },
+ key: 'IN-with-dash',
+ doc_count: 2830,
+ },
+ ],
+ },
+ },
+ status: 200,
+};
+
const nestedTermResponseNoResults = {
took: 10,
timed_out: false,
@@ -326,6 +377,17 @@ describe('Terms Agg Other bucket helper', () => {
}
});
+ test('does not build query if sum_other_doc_count is 0 (exhaustive terms)', () => {
+ const aggConfigs = getAggConfigs(nestedTerm.aggs);
+ expect(
+ buildOtherBucketAgg(
+ aggConfigs,
+ aggConfigs.aggs[1] as IBucketAggConfig,
+ exhaustiveNestedTermResponse
+ )
+ ).toBeFalsy();
+ });
+
test('excludes exists filter for scripted fields', () => {
const aggConfigs = getAggConfigs(nestedTerm.aggs);
aggConfigs.aggs[1].params.field.scripted = true;
diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts
index 372d487bcf7a3..2a1cd873f6282 100644
--- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts
+++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts
@@ -156,6 +156,7 @@ export const buildOtherBucketAgg = (
};
let noAggBucketResults = false;
+ let exhaustiveBuckets = true;
// recursively create filters for all parent aggregation buckets
const walkBucketTree = (
@@ -175,6 +176,9 @@ export const buildOtherBucketAgg = (
const newAggIndex = aggIndex + 1;
const newAgg = bucketAggs[newAggIndex];
const currentAgg = bucketAggs[aggIndex];
+ if (aggIndex === index && agg && agg.sum_other_doc_count > 0) {
+ exhaustiveBuckets = false;
+ }
if (aggIndex < index) {
each(agg.buckets, (bucket: any, bucketObjKey) => {
const bucketKey = currentAgg.getKey(
@@ -223,7 +227,7 @@ export const buildOtherBucketAgg = (
walkBucketTree(0, response.aggregations, bucketAggs[0].id, [], '');
// bail if there were no bucket results
- if (noAggBucketResults) {
+ if (noAggBucketResults || exhaustiveBuckets) {
return false;
}
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index ba873952c9841..078dd3a9b7c5a 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -276,9 +276,8 @@ export { DuplicateIndexPatternError } from '../common/index_patterns/errors';
* Autocomplete query suggestions:
*/
-export {
+export type {
QuerySuggestion,
- QuerySuggestionTypes,
QuerySuggestionGetFn,
QuerySuggestionGetFnArgs,
QuerySuggestionBasic,
@@ -286,6 +285,7 @@ export {
AutocompleteStart,
} from './autocomplete';
+export { QuerySuggestionTypes } from './autocomplete';
/*
* Search:
*/
@@ -320,25 +320,23 @@ import {
tabifyGetColumns,
} from '../common';
-export {
+export { AggGroupLabels, AggGroupNames, METRIC_TYPES, BUCKET_TYPES } from '../common';
+
+export type {
// aggs
AggConfigSerialized,
- AggGroupLabels,
AggGroupName,
- AggGroupNames,
AggFunctionsMapping,
AggParam,
AggParamOption,
AggParamType,
AggConfigOptions,
- BUCKET_TYPES,
EsaggsExpressionFunctionDefinition,
IAggConfig,
IAggConfigs,
IAggType,
IFieldParamType,
IMetricAggType,
- METRIC_TYPES,
OptionedParamType,
OptionedValueProp,
ParsedInterval,
@@ -352,30 +350,23 @@ export {
export type { AggConfigs, AggConfig } from '../common';
-export {
+export type {
// search
ES_SEARCH_STRATEGY,
EsQuerySortValue,
- extractSearchSourceReferences,
- getEsPreference,
- getSearchParamsFromRequest,
IEsSearchRequest,
IEsSearchResponse,
IKibanaSearchRequest,
IKibanaSearchResponse,
- injectSearchSourceReferences,
ISearchSetup,
ISearchStart,
ISearchStartSearchSource,
ISearchGeneric,
ISearchSource,
- parseSearchSourceJSON,
SearchInterceptor,
SearchInterceptorDeps,
SearchRequest,
SearchSourceFields,
- SortDirection,
- SearchSessionState,
// expression functions and types
EsdslExpressionFunctionDefinition,
EsRawResponseExpressionTypeDefinition,
@@ -386,11 +377,21 @@ export {
TimeoutErrorMode,
PainlessError,
Reason,
+ WaitUntilNextSessionCompletesOptions,
+} from './search';
+
+export {
+ parseSearchSourceJSON,
+ injectSearchSourceReferences,
+ extractSearchSourceReferences,
+ getEsPreference,
+ getSearchParamsFromRequest,
noSearchSessionStorageCapabilityMessage,
SEARCH_SESSIONS_MANAGEMENT_ID,
waitUntilNextSessionCompletes$,
- WaitUntilNextSessionCompletesOptions,
isEsError,
+ SearchSessionState,
+ SortDirection,
} from './search';
export type {
@@ -438,33 +439,36 @@ export const search = {
* UI components
*/
-export {
- SearchBar,
+export type {
SearchBarProps,
StatefulSearchBarProps,
IndexPatternSelectProps,
- QueryStringInput,
QueryStringInputProps,
} from './ui';
+export { QueryStringInput, SearchBar } from './ui';
+
/**
* Types to be shared externally
* @public
*/
-export { Filter, Query, RefreshInterval, TimeRange } from '../common';
+export type { Filter, Query, RefreshInterval, TimeRange } from '../common';
export {
createSavedQueryService,
connectToQueryState,
syncQueryStateWithUrl,
- QueryState,
getDefaultQuery,
FilterManager,
+ TimeHistory,
+} from './query';
+
+export type {
+ QueryState,
SavedQuery,
SavedQueryService,
SavedQueryTimeFilter,
InputTimeRange,
- TimeHistory,
TimefilterContract,
TimeHistoryContract,
QueryStateChange,
@@ -472,7 +476,7 @@ export {
AutoRefreshDoneFn,
} from './query';
-export { AggsStart } from './search/aggs';
+export type { AggsStart } from './search/aggs';
export {
getTime,
@@ -496,7 +500,7 @@ export function plugin(initializerContext: PluginInitializerContext>;
-export type Start = jest.Mocked>;
+export type Setup = jest.Mocked>;
+export type Start = jest.Mocked>;
const autocompleteSetupMock: jest.Mocked = {
getQuerySuggestions: jest.fn(),
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 67534577d99fc..13352d183370b 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -67,7 +67,7 @@ import { Observable } from 'rxjs';
import { PackageInfo } from '@kbn/config';
import { Path } from 'history';
import { PeerCertificate } from 'tls';
-import { Plugin as Plugin_2 } from 'src/core/public';
+import { Plugin } from 'src/core/public';
import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public';
import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public';
import { PopoverAnchorPosition } from '@elastic/eui';
@@ -621,6 +621,22 @@ export type CustomFilter = Filter & {
query: any;
};
+// Warning: (ae-forgotten-export) The symbol "DataSetupDependencies" needs to be exported by the entry point index.d.ts
+// Warning: (ae-forgotten-export) The symbol "DataStartDependencies" needs to be exported by the entry point index.d.ts
+// Warning: (ae-missing-release-tag) "DataPublicPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export class DataPlugin implements Plugin {
+ // Warning: (ae-forgotten-export) The symbol "ConfigSchema" needs to be exported by the entry point index.d.ts
+ constructor(initializerContext: PluginInitializerContext_2);
+ // (undocumented)
+ setup(core: CoreSetup, { bfetch, expressions, uiActions, usageCollection, inspector }: DataSetupDependencies): DataPublicPluginSetup;
+ // (undocumented)
+ start(core: CoreStart_2, { uiActions }: DataStartDependencies): DataPublicPluginStart;
+ // (undocumented)
+ stop(): void;
+ }
+
// Warning: (ae-missing-release-tag) "DataPublicPluginSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -2004,27 +2020,11 @@ export type PhrasesFilter = Filter & {
meta: PhrasesFilterMeta;
};
-// Warning: (ae-forgotten-export) The symbol "DataSetupDependencies" needs to be exported by the entry point index.d.ts
-// Warning: (ae-forgotten-export) The symbol "DataStartDependencies" needs to be exported by the entry point index.d.ts
-// Warning: (ae-missing-release-tag) "DataPublicPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
-//
-// @public (undocumented)
-export class Plugin implements Plugin_2 {
- // Warning: (ae-forgotten-export) The symbol "ConfigSchema" needs to be exported by the entry point index.d.ts
- constructor(initializerContext: PluginInitializerContext_2);
- // (undocumented)
- setup(core: CoreSetup, { bfetch, expressions, uiActions, usageCollection, inspector }: DataSetupDependencies): DataPublicPluginSetup;
- // (undocumented)
- start(core: CoreStart_2, { uiActions }: DataStartDependencies): DataPublicPluginStart;
- // (undocumented)
- stop(): void;
- }
-
// Warning: (ae-forgotten-export) The symbol "PluginInitializerContext" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "plugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export function plugin(initializerContext: PluginInitializerContext): Plugin;
+export function plugin(initializerContext: PluginInitializerContext): DataPlugin;
// Warning: (ae-missing-release-tag) "Query" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -2772,20 +2772,20 @@ export interface WaitUntilNextSessionCompletesOptions {
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:407:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:407:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:407:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:434:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:408:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:408:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:408:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:432:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:435:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
index 1214625fe530f..8cf2de8c80743 100644
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
+++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
@@ -14,6 +14,7 @@ import deepEqual from 'fast-deep-equal';
import { buildContextMenuForActions, UiActionsService, Action } from '../ui_actions';
import { CoreStart, OverlayStart } from '../../../../../core/public';
import { toMountPoint } from '../../../../kibana_react/public';
+import { UsageCollectionStart } from '../../../../usage_collection/public';
import { Start as InspectorStartContract } from '../inspector';
import {
@@ -62,6 +63,7 @@ interface Props {
SavedObjectFinder: React.ComponentType;
stateTransfer?: EmbeddableStateTransfer;
hideHeader?: boolean;
+ reportUiCounter?: UsageCollectionStart['reportUiCounter'];
}
interface State {
@@ -312,7 +314,8 @@ export class EmbeddablePanel extends React.Component {
this.props.getAllEmbeddableFactories,
this.props.overlays,
this.props.notifications,
- this.props.SavedObjectFinder
+ this.props.SavedObjectFinder,
+ this.props.reportUiCounter
),
inspectPanel: new InspectPanelAction(this.props.inspector),
removePanel: new RemovePanelAction(),
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts
index 8b6f81a199c44..49be1c3ce0123 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts
@@ -13,6 +13,7 @@ import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin';
import { ViewMode } from '../../../../types';
import { openAddPanelFlyout } from './open_add_panel_flyout';
import { IContainer } from '../../../../containers';
+import { UsageCollectionStart } from '../../../../../../../usage_collection/public';
export const ACTION_ADD_PANEL = 'ACTION_ADD_PANEL';
@@ -29,7 +30,8 @@ export class AddPanelAction implements Action {
private readonly getAllFactories: EmbeddableStart['getEmbeddableFactories'],
private readonly overlays: OverlayStart,
private readonly notifications: NotificationsStart,
- private readonly SavedObjectFinder: React.ComponentType
+ private readonly SavedObjectFinder: React.ComponentType,
+ private readonly reportUiCounter?: UsageCollectionStart['reportUiCounter']
) {}
public getDisplayName() {
@@ -60,6 +62,7 @@ export class AddPanelAction implements Action {
overlays: this.overlays,
notifications: this.notifications,
SavedObjectFinder: this.SavedObjectFinder,
+ reportUiCounter: this.reportUiCounter,
});
}
}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx
index 6d6a68d7e5e2a..eb4f0b30c5110 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx
@@ -9,15 +9,17 @@
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { ReactElement } from 'react';
-import { CoreSetup } from 'src/core/public';
+import { METRIC_TYPE } from '@kbn/analytics';
+import { CoreSetup, SavedObjectAttributes, SimpleSavedObject } from 'src/core/public';
import { EuiContextMenuItem, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
-import { EmbeddableStart } from 'src/plugins/embeddable/public';
+import { EmbeddableFactory, EmbeddableStart } from 'src/plugins/embeddable/public';
import { IContainer } from '../../../../containers';
import { EmbeddableFactoryNotFoundError } from '../../../../errors';
import { SavedObjectFinderCreateNew } from './saved_object_finder_create_new';
import { SavedObjectEmbeddableInput } from '../../../../embeddables';
+import { UsageCollectionStart } from '../../../../../../../usage_collection/public';
interface Props {
onClose: () => void;
@@ -27,6 +29,7 @@ interface Props {
notifications: CoreSetup['notifications'];
SavedObjectFinder: React.ComponentType;
showCreateNewMenu?: boolean;
+ reportUiCounter?: UsageCollectionStart['reportUiCounter'];
}
interface State {
@@ -84,7 +87,12 @@ export class AddPanelFlyout extends React.Component {
}
};
- public onAddPanel = async (savedObjectId: string, savedObjectType: string, name: string) => {
+ public onAddPanel = async (
+ savedObjectId: string,
+ savedObjectType: string,
+ name: string,
+ so: SimpleSavedObject
+ ) => {
const factoryForSavedObjectType = [...this.props.getAllFactories()].find(
(factory) =>
factory.savedObjectMetaData && factory.savedObjectMetaData.type === savedObjectType
@@ -98,9 +106,27 @@ export class AddPanelFlyout extends React.Component {
{ savedObjectId }
);
+ this.doTelemetryForAddEvent(this.props.container.type, factoryForSavedObjectType, so);
+
this.showToast(name);
};
+ private doTelemetryForAddEvent(
+ appName: string,
+ factoryForSavedObjectType: EmbeddableFactory,
+ so: SimpleSavedObject
+ ) {
+ const { reportUiCounter } = this.props;
+
+ if (reportUiCounter) {
+ const type = factoryForSavedObjectType.savedObjectMetaData?.getSavedObjectSubType
+ ? factoryForSavedObjectType.savedObjectMetaData.getSavedObjectSubType(so)
+ : factoryForSavedObjectType.type;
+
+ reportUiCounter(appName, METRIC_TYPE.CLICK, `${type}:add`);
+ }
+ }
+
private getCreateMenuItems(): ReactElement[] {
return [...this.props.getAllFactories()]
.filter(
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx
index f0c6e81644b3d..fe54b3d134aa0 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx
@@ -12,6 +12,7 @@ import { EmbeddableStart } from '../../../../../plugin';
import { toMountPoint } from '../../../../../../../kibana_react/public';
import { IContainer } from '../../../../containers';
import { AddPanelFlyout } from './add_panel_flyout';
+import { UsageCollectionStart } from '../../../../../../../usage_collection/public';
export function openAddPanelFlyout(options: {
embeddable: IContainer;
@@ -21,6 +22,7 @@ export function openAddPanelFlyout(options: {
notifications: NotificationsStart;
SavedObjectFinder: React.ComponentType;
showCreateNewMenu?: boolean;
+ reportUiCounter?: UsageCollectionStart['reportUiCounter'];
}): OverlayRef {
const {
embeddable,
@@ -30,6 +32,7 @@ export function openAddPanelFlyout(options: {
notifications,
SavedObjectFinder,
showCreateNewMenu,
+ reportUiCounter,
} = options;
const flyoutSession = overlays.openFlyout(
toMountPoint(
@@ -43,6 +46,7 @@ export function openAddPanelFlyout(options: {
getFactory={getFactory}
getAllFactories={getAllFactories}
notifications={notifications}
+ reportUiCounter={reportUiCounter}
SavedObjectFinder={SavedObjectFinder}
showCreateNewMenu={showCreateNewMenu}
/>
diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md
index 2a577e6167be5..af708f9a5e659 100644
--- a/src/plugins/embeddable/public/public.api.md
+++ b/src/plugins/embeddable/public/public.api.md
@@ -63,6 +63,7 @@ import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
import { Type } from '@kbn/config-schema';
import { TypeOf } from '@kbn/config-schema';
import { UiComponent } from 'src/plugins/kibana_utils/public';
+import { UiCounterMetricType } from '@kbn/analytics';
import { UnregisterCallback } from 'history';
import { URL } from 'url';
import { UserProvidedValues } from 'src/core/server/types';
@@ -95,7 +96,7 @@ export interface Adapters {
// @public (undocumented)
export class AddPanelAction implements Action_3 {
// Warning: (ae-forgotten-export) The symbol "React" needs to be exported by the entry point index.d.ts
- constructor(getFactory: EmbeddableStart_2['getEmbeddableFactory'], getAllFactories: EmbeddableStart_2['getEmbeddableFactories'], overlays: OverlayStart_2, notifications: NotificationsStart_2, SavedObjectFinder: React_2.ComponentType);
+ constructor(getFactory: EmbeddableStart_2['getEmbeddableFactory'], getAllFactories: EmbeddableStart_2['getEmbeddableFactories'], overlays: OverlayStart_2, notifications: NotificationsStart_2, SavedObjectFinder: React_2.ComponentType, reportUiCounter?: ((appName: string, type: import("@kbn/analytics").UiCounterMetricType, eventNames: string | string[], count?: number | undefined) => void) | undefined);
// (undocumented)
execute(context: ActionExecutionContext_2): Promise;
// (undocumented)
@@ -729,6 +730,7 @@ export function openAddPanelFlyout(options: {
notifications: NotificationsStart_2;
SavedObjectFinder: React.ComponentType;
showCreateNewMenu?: boolean;
+ reportUiCounter?: UsageCollectionStart['reportUiCounter'];
}): OverlayRef_2;
// Warning: (ae-missing-release-tag) "OutputSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -890,6 +892,7 @@ export const withEmbeddableSubscription: .
+ */
+
+export const PageError: React.FunctionComponent = ({
+ title,
+ error,
+ actions,
+ isCentered,
+ ...rest
+}) => {
+ const {
+ error: errorString,
+ cause, // wrapEsError() on the server adds a "cause" array
+ message,
+ } = error;
+
+ const errorContent = (
+
+ {title}}
+ body={
+ <>
+ {cause ? message || errorString : {message || errorString}
}
+ {cause && (
+ <>
+
+
+ {cause.map((causeMsg, i) => (
+ {causeMsg}
+ ))}
+
+ >
+ )}
+ >
+ }
+ iconType="alert"
+ actions={actions}
+ {...rest}
+ />
+
+ );
+
+ if (isCentered) {
+ return {errorContent}
;
+ }
+
+ return errorContent;
+};
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx
index c0b3533c8594b..a1652b4e153f5 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx
@@ -8,12 +8,7 @@
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import React, { Fragment } from 'react';
-
-export interface Error {
- error: string;
- cause?: string[];
- message?: string;
-}
+import { Error } from '../types';
interface Props {
title: React.ReactNode;
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts
index 089dc890c3e6c..e63d98512a2cd 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts
@@ -12,8 +12,8 @@ export {
AuthorizationProvider,
AuthorizationContext,
SectionError,
- Error,
+ PageError,
useAuthorizationContext,
} from './components';
-export { Privileges, MissingPrivileges } from './types';
+export { Privileges, MissingPrivileges, Error } from './types';
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts
index b10318aa415b3..70b54b0b6e425 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts
@@ -14,3 +14,9 @@ export interface Privileges {
hasAllPrivileges: boolean;
missingPrivileges: MissingPrivileges;
}
+
+export interface Error {
+ error: string;
+ cause?: string[];
+ message?: string;
+}
diff --git a/src/plugins/es_ui_shared/public/authorization/index.ts b/src/plugins/es_ui_shared/public/authorization/index.ts
index 483fffd9c4859..f68ad3da2a4b5 100644
--- a/src/plugins/es_ui_shared/public/authorization/index.ts
+++ b/src/plugins/es_ui_shared/public/authorization/index.ts
@@ -14,6 +14,7 @@ export {
NotAuthorizedSection,
Privileges,
SectionError,
+ PageError,
useAuthorizationContext,
WithPrivileges,
} from '../../__packages_do_not_import__/authorization';
diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts
index b46a23994fe93..7b9013c043a0e 100644
--- a/src/plugins/es_ui_shared/public/index.ts
+++ b/src/plugins/es_ui_shared/public/index.ts
@@ -40,6 +40,7 @@ export {
Privileges,
MissingPrivileges,
SectionError,
+ PageError,
Error,
useAuthorizationContext,
} from './authorization';
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
index 181bd9959c1bb..fb334afb22b13 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
@@ -18,7 +18,7 @@ const DEFAULT_OPTIONS = {
stripEmptyFields: true,
};
-interface UseFormReturn {
+export interface UseFormReturn {
form: FormHook;
}
diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts
index 69687f75f3098..feff425cc48ed 100644
--- a/src/plugins/expressions/common/execution/execution.test.ts
+++ b/src/plugins/expressions/common/execution/execution.test.ts
@@ -834,8 +834,8 @@ describe('Execution', () => {
expect((chain[0].arguments.val[0] as ExpressionAstExpression).chain[0].debug!.args).toEqual(
{
- name: 'foo',
- value: 5,
+ name: ['foo'],
+ value: [5],
}
);
});
diff --git a/src/plugins/expressions/common/expression_functions/specs/index.ts b/src/plugins/expressions/common/expression_functions/specs/index.ts
index 20a6f9aac4567..c6d89f41d0e0d 100644
--- a/src/plugins/expressions/common/expression_functions/specs/index.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/index.ts
@@ -12,6 +12,7 @@ export * from './var_set';
export * from './var';
export * from './theme';
export * from './cumulative_sum';
+export * from './overall_metric';
export * from './derivative';
export * from './moving_average';
export * from './ui_setting';
diff --git a/src/plugins/expressions/common/expression_functions/specs/overall_metric.ts b/src/plugins/expressions/common/expression_functions/specs/overall_metric.ts
new file mode 100644
index 0000000000000..e42112d3a23ed
--- /dev/null
+++ b/src/plugins/expressions/common/expression_functions/specs/overall_metric.ts
@@ -0,0 +1,168 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { ExpressionFunctionDefinition } from '../types';
+import { Datatable } from '../../expression_types';
+import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
+
+export interface OverallMetricArgs {
+ by?: string[];
+ inputColumnId: string;
+ outputColumnId: string;
+ outputColumnName?: string;
+ metric: 'sum' | 'min' | 'max' | 'average';
+}
+
+export type ExpressionFunctionOverallMetric = ExpressionFunctionDefinition<
+ 'overall_metric',
+ Datatable,
+ OverallMetricArgs,
+ Datatable
+>;
+
+function getValueAsNumberArray(value: unknown) {
+ if (Array.isArray(value)) {
+ return value.map((innerVal) => Number(innerVal));
+ } else {
+ return [Number(value)];
+ }
+}
+
+/**
+ * Calculates the overall metric of a specified column in the data table.
+ *
+ * Also supports multiple series in a single data table - use the `by` argument
+ * to specify the columns to split the calculation by.
+ * For each unique combination of all `by` columns a separate overall metric will be calculated.
+ * The order of rows won't be changed - this function is not modifying any existing columns, it's only
+ * adding the specified `outputColumnId` column to every row of the table without adding or removing rows.
+ *
+ * Behavior:
+ * * Will write the overall metric of `inputColumnId` into `outputColumnId`
+ * * If provided will use `outputColumnName` as name for the newly created column. Otherwise falls back to `outputColumnId`
+ * * Each cell will contain the calculated metric based on the values of all cells belonging to the current series.
+ *
+ * Edge cases:
+ * * Will return the input table if `inputColumnId` does not exist
+ * * Will throw an error if `outputColumnId` exists already in provided data table
+ * * If the row value contains `null` or `undefined`, it will be ignored and overwritten with the overall metric of
+ * all cells of the same series.
+ * * For all values besides `null` and `undefined`, the value will be cast to a number before it's added to the
+ * overall metric of the current series - if this results in `NaN` (like in case of objects), all cells of the
+ * current series will be set to `NaN`.
+ * * To determine separate series defined by the `by` columns, the values of these columns will be cast to strings
+ * before comparison. If the values are objects, the return value of their `toString` method will be used for comparison.
+ * Missing values (`null` and `undefined`) will be treated as empty strings.
+ */
+export const overallMetric: ExpressionFunctionOverallMetric = {
+ name: 'overall_metric',
+ type: 'datatable',
+
+ inputTypes: ['datatable'],
+
+ help: i18n.translate('expressions.functions.overallMetric.help', {
+ defaultMessage: 'Calculates the overall sum, min, max or average of a column in a data table',
+ }),
+
+ args: {
+ by: {
+ help: i18n.translate('expressions.functions.overallMetric.args.byHelpText', {
+ defaultMessage: 'Column to split the overall calculation by',
+ }),
+ multi: true,
+ types: ['string'],
+ required: false,
+ },
+ metric: {
+ help: i18n.translate('expressions.functions.overallMetric.metricHelpText', {
+ defaultMessage: 'Metric to calculate',
+ }),
+ types: ['string'],
+ options: ['sum', 'min', 'max', 'average'],
+ },
+ inputColumnId: {
+ help: i18n.translate('expressions.functions.overallMetric.args.inputColumnIdHelpText', {
+ defaultMessage: 'Column to calculate the overall metric of',
+ }),
+ types: ['string'],
+ required: true,
+ },
+ outputColumnId: {
+ help: i18n.translate('expressions.functions.overallMetric.args.outputColumnIdHelpText', {
+ defaultMessage: 'Column to store the resulting overall metric in',
+ }),
+ types: ['string'],
+ required: true,
+ },
+ outputColumnName: {
+ help: i18n.translate('expressions.functions.overallMetric.args.outputColumnNameHelpText', {
+ defaultMessage: 'Name of the column to store the resulting overall metric in',
+ }),
+ types: ['string'],
+ required: false,
+ },
+ },
+
+ fn(input, { by, inputColumnId, outputColumnId, outputColumnName, metric }) {
+ const resultColumns = buildResultColumns(
+ input,
+ outputColumnId,
+ inputColumnId,
+ outputColumnName
+ );
+
+ if (!resultColumns) {
+ return input;
+ }
+
+ const accumulators: Partial> = {};
+ const valueCounter: Partial> = {};
+ input.rows.forEach((row) => {
+ const bucketIdentifier = getBucketIdentifier(row, by);
+ const accumulatorValue = accumulators[bucketIdentifier] ?? 0;
+
+ const currentValue = row[inputColumnId];
+ if (currentValue != null) {
+ const currentNumberValues = getValueAsNumberArray(currentValue);
+ switch (metric) {
+ case 'average':
+ valueCounter[bucketIdentifier] =
+ (valueCounter[bucketIdentifier] ?? 0) + currentNumberValues.length;
+ case 'sum':
+ accumulators[bucketIdentifier] =
+ accumulatorValue + currentNumberValues.reduce((a, b) => a + b, 0);
+ break;
+ case 'min':
+ accumulators[bucketIdentifier] = Math.min(accumulatorValue, ...currentNumberValues);
+ break;
+ case 'max':
+ accumulators[bucketIdentifier] = Math.max(accumulatorValue, ...currentNumberValues);
+ break;
+ }
+ }
+ });
+ if (metric === 'average') {
+ Object.keys(accumulators).forEach((bucketIdentifier) => {
+ accumulators[bucketIdentifier] =
+ accumulators[bucketIdentifier]! / valueCounter[bucketIdentifier]!;
+ });
+ }
+ return {
+ ...input,
+ columns: resultColumns,
+ rows: input.rows.map((row) => {
+ const newRow = { ...row };
+ const bucketIdentifier = getBucketIdentifier(row, by);
+ newRow[outputColumnId] = accumulators[bucketIdentifier];
+
+ return newRow;
+ }),
+ };
+ },
+};
diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/overall_metric.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/overall_metric.test.ts
new file mode 100644
index 0000000000000..30354c4e54dc7
--- /dev/null
+++ b/src/plugins/expressions/common/expression_functions/specs/tests/overall_metric.test.ts
@@ -0,0 +1,450 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { functionWrapper } from './utils';
+import { ExecutionContext } from '../../../execution/types';
+import { Datatable } from '../../../expression_types/specs/datatable';
+import { overallMetric, OverallMetricArgs } from '../overall_metric';
+
+describe('interpreter/functions#overall_metric', () => {
+ const fn = functionWrapper(overallMetric);
+ const runFn = (input: Datatable, args: OverallMetricArgs) =>
+ fn(input, args, {} as ExecutionContext) as Datatable;
+
+ it('calculates overall sum', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
+ rows: [{ val: 5 }, { val: 7 }, { val: 3 }, { val: 2 }],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', metric: 'sum' }
+ );
+ expect(result.columns).toContainEqual({
+ id: 'output',
+ name: 'output',
+ meta: { type: 'number' },
+ });
+ expect(result.rows.map((row) => row.output)).toEqual([17, 17, 17, 17]);
+ });
+
+ it('ignores null or undefined', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
+ rows: [{}, { val: null }, { val: undefined }, { val: 1 }, { val: 5 }],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', metric: 'average' }
+ );
+ expect(result.columns).toContainEqual({
+ id: 'output',
+ name: 'output',
+ meta: { type: 'number' },
+ });
+ expect(result.rows.map((row) => row.output)).toEqual([3, 3, 3, 3, 3]);
+ });
+
+ it('calculates overall sum for multiple series', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [
+ { id: 'val', name: 'val', meta: { type: 'number' } },
+ { id: 'split', name: 'split', meta: { type: 'string' } },
+ ],
+ rows: [
+ { val: 1, split: 'A' },
+ { val: 2, split: 'B' },
+ { val: 3, split: 'B' },
+ { val: 4, split: 'A' },
+ { val: 5, split: 'A' },
+ { val: 6, split: 'A' },
+ { val: 7, split: 'B' },
+ { val: 8, split: 'B' },
+ ],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', by: ['split'], metric: 'sum' }
+ );
+
+ expect(result.rows.map((row) => row.output)).toEqual([
+ 1 + 4 + 5 + 6,
+ 2 + 3 + 7 + 8,
+ 2 + 3 + 7 + 8,
+ 1 + 4 + 5 + 6,
+ 1 + 4 + 5 + 6,
+ 1 + 4 + 5 + 6,
+ 2 + 3 + 7 + 8,
+ 2 + 3 + 7 + 8,
+ ]);
+ });
+
+ it('treats missing split column as separate series', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [
+ { id: 'val', name: 'val', meta: { type: 'number' } },
+ { id: 'split', name: 'split', meta: { type: 'string' } },
+ ],
+ rows: [
+ { val: 1, split: 'A' },
+ { val: 2, split: 'B' },
+ { val: 3 },
+ { val: 4, split: 'A' },
+ { val: 5 },
+ { val: 6, split: 'A' },
+ { val: 7, split: 'B' },
+ { val: 8, split: 'B' },
+ ],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', by: ['split'], metric: 'sum' }
+ );
+ expect(result.rows.map((row) => row.output)).toEqual([
+ 1 + 4 + 6,
+ 2 + 7 + 8,
+ 3 + 5,
+ 1 + 4 + 6,
+ 3 + 5,
+ 1 + 4 + 6,
+ 2 + 7 + 8,
+ 2 + 7 + 8,
+ ]);
+ });
+
+ it('treats null like undefined and empty string for split columns', () => {
+ const table: Datatable = {
+ type: 'datatable',
+ columns: [
+ { id: 'val', name: 'val', meta: { type: 'number' } },
+ { id: 'split', name: 'split', meta: { type: 'string' } },
+ ],
+ rows: [
+ { val: 1, split: 'A' },
+ { val: 2, split: 'B' },
+ { val: 3 },
+ { val: 4, split: 'A' },
+ { val: 5 },
+ { val: 6, split: 'A' },
+ { val: 7, split: null },
+ { val: 8, split: 'B' },
+ { val: 9, split: '' },
+ ],
+ };
+
+ const result = runFn(table, {
+ inputColumnId: 'val',
+ outputColumnId: 'output',
+ by: ['split'],
+ metric: 'sum',
+ });
+ expect(result.rows.map((row) => row.output)).toEqual([
+ 1 + 4 + 6,
+ 2 + 8,
+ 3 + 5 + 7 + 9,
+ 1 + 4 + 6,
+ 3 + 5 + 7 + 9,
+ 1 + 4 + 6,
+ 3 + 5 + 7 + 9,
+ 2 + 8,
+ 3 + 5 + 7 + 9,
+ ]);
+
+ const result2 = runFn(table, {
+ inputColumnId: 'val',
+ outputColumnId: 'output',
+ by: ['split'],
+ metric: 'max',
+ });
+ expect(result2.rows.map((row) => row.output)).toEqual([6, 8, 9, 6, 9, 6, 9, 8, 9]);
+ });
+
+ it('handles array values', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
+ rows: [{ val: 5 }, { val: [7, 10] }, { val: [3, 1] }, { val: 2 }],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', metric: 'sum' }
+ );
+ expect(result.columns).toContainEqual({
+ id: 'output',
+ name: 'output',
+ meta: { type: 'number' },
+ });
+ expect(result.rows.map((row) => row.output)).toEqual([28, 28, 28, 28]);
+ });
+
+ it('takes array values into account for average calculation', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
+ rows: [{ val: [3, 4] }, { val: 2 }],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', metric: 'average' }
+ );
+ expect(result.columns).toContainEqual({
+ id: 'output',
+ name: 'output',
+ meta: { type: 'number' },
+ });
+ expect(result.rows.map((row) => row.output)).toEqual([3, 3]);
+ });
+
+ it('handles array values for split columns', () => {
+ const table: Datatable = {
+ type: 'datatable',
+ columns: [
+ { id: 'val', name: 'val', meta: { type: 'number' } },
+ { id: 'split', name: 'split', meta: { type: 'string' } },
+ ],
+ rows: [
+ { val: 1, split: 'A' },
+ { val: [2, 11], split: 'B' },
+ { val: 3 },
+ { val: 4, split: 'A' },
+ { val: 5 },
+ { val: 6, split: 'A' },
+ { val: 7, split: null },
+ { val: 8, split: 'B' },
+ { val: [9, 99], split: '' },
+ ],
+ };
+
+ const result = runFn(table, {
+ inputColumnId: 'val',
+ outputColumnId: 'output',
+ by: ['split'],
+ metric: 'sum',
+ });
+ expect(result.rows.map((row) => row.output)).toEqual([
+ 1 + 4 + 6,
+ 2 + 11 + 8,
+ 3 + 5 + 7 + 9 + 99,
+ 1 + 4 + 6,
+ 3 + 5 + 7 + 9 + 99,
+ 1 + 4 + 6,
+ 3 + 5 + 7 + 9 + 99,
+ 2 + 11 + 8,
+ 3 + 5 + 7 + 9 + 99,
+ ]);
+
+ const result2 = runFn(table, {
+ inputColumnId: 'val',
+ outputColumnId: 'output',
+ by: ['split'],
+ metric: 'max',
+ });
+ expect(result2.rows.map((row) => row.output)).toEqual([6, 11, 99, 6, 99, 6, 99, 11, 99]);
+ });
+
+ it('calculates cumulative sum for multiple series by multiple split columns', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [
+ { id: 'val', name: 'val', meta: { type: 'number' } },
+ { id: 'split', name: 'split', meta: { type: 'string' } },
+ { id: 'split2', name: 'split2', meta: { type: 'string' } },
+ ],
+ rows: [
+ { val: 1, split: 'A', split2: 'C' },
+ { val: 2, split: 'B', split2: 'C' },
+ { val: 3, split2: 'C' },
+ { val: 4, split: 'A', split2: 'C' },
+ { val: 5 },
+ { val: 6, split: 'A', split2: 'D' },
+ { val: 7, split: 'B', split2: 'D' },
+ { val: 8, split: 'B', split2: 'D' },
+ ],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', by: ['split', 'split2'], metric: 'sum' }
+ );
+ expect(result.rows.map((row) => row.output)).toEqual([1 + 4, 2, 3, 1 + 4, 5, 6, 7 + 8, 7 + 8]);
+ });
+
+ it('splits separate series by the string representation of the cell values', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [
+ { id: 'val', name: 'val', meta: { type: 'number' } },
+ { id: 'split', name: 'split', meta: { type: 'string' } },
+ ],
+ rows: [
+ { val: 1, split: { anObj: 3 } },
+ { val: 2, split: { anotherObj: 5 } },
+ { val: 10, split: 5 },
+ { val: 11, split: '5' },
+ ],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', by: ['split'], metric: 'sum' }
+ );
+
+ expect(result.rows.map((row) => row.output)).toEqual([1 + 2, 1 + 2, 10 + 11, 10 + 11]);
+ });
+
+ it('casts values to number before calculating cumulative sum', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
+ rows: [{ val: 5 }, { val: '7' }, { val: '3' }, { val: 2 }],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', metric: 'max' }
+ );
+ expect(result.rows.map((row) => row.output)).toEqual([7, 7, 7, 7]);
+ });
+
+ it('casts values to number before calculating metric for NaN like values', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
+ rows: [{ val: 5 }, { val: '7' }, { val: {} }, { val: 2 }],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', metric: 'min' }
+ );
+ expect(result.rows.map((row) => row.output)).toEqual([NaN, NaN, NaN, NaN]);
+ });
+
+ it('skips undefined and null values', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
+ rows: [
+ { val: null },
+ { val: 7 },
+ { val: undefined },
+ { val: undefined },
+ { val: undefined },
+ { val: undefined },
+ { val: '3' },
+ { val: 2 },
+ { val: null },
+ ],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', metric: 'average' }
+ );
+ expect(result.rows.map((row) => row.output)).toEqual([4, 4, 4, 4, 4, 4, 4, 4, 4]);
+ });
+
+ it('copies over meta information from the source column', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [
+ {
+ id: 'val',
+ name: 'val',
+ meta: {
+ type: 'number',
+
+ field: 'afield',
+ index: 'anindex',
+ params: { id: 'number', params: { pattern: '000' } },
+ source: 'synthetic',
+ sourceParams: {
+ some: 'params',
+ },
+ },
+ },
+ ],
+ rows: [{ val: 5 }],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', metric: 'sum' }
+ );
+ expect(result.columns).toContainEqual({
+ id: 'output',
+ name: 'output',
+ meta: {
+ type: 'number',
+
+ field: 'afield',
+ index: 'anindex',
+ params: { id: 'number', params: { pattern: '000' } },
+ source: 'synthetic',
+ sourceParams: {
+ some: 'params',
+ },
+ },
+ });
+ });
+
+ it('sets output name on output column if specified', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [
+ {
+ id: 'val',
+ name: 'val',
+ meta: {
+ type: 'number',
+ },
+ },
+ ],
+ rows: [{ val: 5 }],
+ },
+ {
+ inputColumnId: 'val',
+ outputColumnId: 'output',
+ outputColumnName: 'Output name',
+ metric: 'min',
+ }
+ );
+ expect(result.columns).toContainEqual({
+ id: 'output',
+ name: 'Output name',
+ meta: { type: 'number' },
+ });
+ });
+
+ it('returns source table if input column does not exist', () => {
+ const input: Datatable = {
+ type: 'datatable',
+ columns: [
+ {
+ id: 'val',
+ name: 'val',
+ meta: {
+ type: 'number',
+ },
+ },
+ ],
+ rows: [{ val: 5 }],
+ };
+ expect(
+ runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output', metric: 'sum' })
+ ).toBe(input);
+ });
+
+ it('throws an error if output column exists already', () => {
+ expect(() =>
+ runFn(
+ {
+ type: 'datatable',
+ columns: [
+ {
+ id: 'val',
+ name: 'val',
+ meta: {
+ type: 'number',
+ },
+ },
+ ],
+ rows: [{ val: 5 }],
+ },
+ { inputColumnId: 'val', outputColumnId: 'val', metric: 'max' }
+ )
+ ).toThrow();
+ });
+});
diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts
index 0a9f022ce89ca..cdcae61215fa4 100644
--- a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts
@@ -9,6 +9,8 @@
import { functionWrapper } from './utils';
import { variableSet } from '../var_set';
import { ExecutionContext } from '../../../execution/types';
+import { createUnitTestExecutor } from '../../../test_helpers';
+import { first } from 'rxjs/operators';
describe('expression_functions', () => {
describe('var_set', () => {
@@ -32,21 +34,49 @@ describe('expression_functions', () => {
});
it('updates a variable', () => {
- const actual = fn(input, { name: 'test', value: 2 }, context);
+ const actual = fn(input, { name: ['test'], value: [2] }, context);
expect(variables.test).toEqual(2);
expect(actual).toEqual(input);
});
it('sets a new variable', () => {
- const actual = fn(input, { name: 'new', value: 3 }, context);
+ const actual = fn(input, { name: ['new'], value: [3] }, context);
expect(variables.new).toEqual(3);
expect(actual).toEqual(input);
});
it('stores context if value is not set', () => {
- const actual = fn(input, { name: 'test' }, context);
+ const actual = fn(input, { name: ['test'], value: [] }, context);
expect(variables.test).toEqual(input);
expect(actual).toEqual(input);
});
+
+ it('sets multiple variables', () => {
+ const actual = fn(input, { name: ['new1', 'new2', 'new3'], value: [1, , 3] }, context);
+ expect(variables.new1).toEqual(1);
+ expect(variables.new2).toEqual(input);
+ expect(variables.new3).toEqual(3);
+ expect(actual).toEqual(input);
+ });
+
+ describe('running function thru executor', () => {
+ const executor = createUnitTestExecutor();
+ executor.registerFunction(variableSet);
+
+ it('sets the variables', async () => {
+ const vars = {};
+ const result = await executor
+ .run('var_set name=test1 name=test2 value=1', 2, { variables: vars })
+ .pipe(first())
+ .toPromise();
+
+ expect(result).toEqual(2);
+
+ expect(vars).toEqual({
+ test1: 1,
+ test2: 2,
+ });
+ });
+ });
});
});
diff --git a/src/plugins/expressions/common/expression_functions/specs/var_set.ts b/src/plugins/expressions/common/expression_functions/specs/var_set.ts
index 490c7781a01a1..f3ac6a2ab80d4 100644
--- a/src/plugins/expressions/common/expression_functions/specs/var_set.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/var_set.ts
@@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from '../types';
interface Arguments {
- name: string;
- value?: any;
+ name: string[];
+ value: any[];
}
export type ExpressionFunctionVarSet = ExpressionFunctionDefinition<
@@ -31,12 +31,14 @@ export const variableSet: ExpressionFunctionVarSet = {
types: ['string'],
aliases: ['_'],
required: true,
+ multi: true,
help: i18n.translate('expressions.functions.varset.name.help', {
defaultMessage: 'Specify the name of the variable.',
}),
},
value: {
aliases: ['val'],
+ multi: true,
help: i18n.translate('expressions.functions.varset.val.help', {
defaultMessage:
'Specify the value for the variable. When unspecified, the input context is used.',
@@ -45,7 +47,9 @@ export const variableSet: ExpressionFunctionVarSet = {
},
fn(input, args, context) {
const variables: Record = context.variables;
- variables[args.name] = args.value === undefined ? input : args.value;
+ args.name.forEach((name, i) => {
+ variables[name] = args.value[i] === undefined ? input : args.value[i];
+ });
return input;
},
};
diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts
index e1378a27bdfc2..0ec61b39608a0 100644
--- a/src/plugins/expressions/common/expression_functions/types.ts
+++ b/src/plugins/expressions/common/expression_functions/types.ts
@@ -18,6 +18,7 @@ import {
ExpressionFunctionCumulativeSum,
ExpressionFunctionDerivative,
ExpressionFunctionMovingAverage,
+ ExpressionFunctionOverallMetric,
} from './specs';
import { ExpressionAstFunction } from '../ast';
import { PersistableStateDefinition } from '../../../kibana_utils/common';
@@ -119,6 +120,7 @@ export interface ExpressionFunctionDefinitions {
var: ExpressionFunctionVar;
theme: ExpressionFunctionTheme;
cumulative_sum: ExpressionFunctionCumulativeSum;
+ overall_metric: ExpressionFunctionOverallMetric;
derivative: ExpressionFunctionDerivative;
moving_average: ExpressionFunctionMovingAverage;
}
diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts
index a8839c9b0d71e..f7afc12aa96ba 100644
--- a/src/plugins/expressions/common/service/expressions_services.ts
+++ b/src/plugins/expressions/common/service/expressions_services.ts
@@ -29,6 +29,7 @@ import {
derivative,
movingAverage,
mapColumn,
+ overallMetric,
math,
} from '../expression_functions';
@@ -340,6 +341,7 @@ export class ExpressionsService implements PersistableStateService [
defaultMessage: '[eCommerce] Top Selling Products',
}),
visState:
- '{"title":"[eCommerce] Top Selling Products","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"products.product_name.keyword","size":7,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
+ '{"title":"[eCommerce] Top Selling Products","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"products.product_name.keyword","size":7,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
uiStateJSON: '{}',
description: '',
version: 1,
diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts
index 05a3d012d707c..816322dbe5299 100644
--- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts
+++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts
@@ -242,7 +242,7 @@ export const getSavedObjects = (): SavedObject[] => [
defaultMessage: '[Flights] Destination Weather',
}),
visState:
- '{"title":"[Flights] Destination Weather","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestWeather","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
+ '{"title":"[Flights] Destination Weather","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestWeather","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
uiStateJSON: '{}',
description: '',
version: 1,
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
index 21248ac9d1dc0..38a9e47014416 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
@@ -14,46 +14,46 @@ exports[`CreateIndexPatternWizard defaults to the loading state 1`] = `
exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = `
-
-
-
-
+
+
-
+ }
+ showSystemIndices={true}
+ />
-
-
-
-
+
+
-
+ }
+ showSystemIndices={true}
+ />
-
-
-
-
+
+
-
+ }
+ />
-
-
-
-
+
+
-
+ }
+ showSystemIndices={true}
+ />
-
-
-
-
+
+
-
+ }
+ showSystemIndices={true}
+ />
}
>
-
-
-
+
Create test index pattern
-
-
-
- Beta
-
-
-
-
-
+
+
+
+
+ }
+ >
+
-
-
+ Create test index pattern
+
+
+
+
+
+ }
+ responsive={true}
>
-
-
- multiple
- ,
- "single":
- filebeat-4-3-22
- ,
- "star":
- filebeat-*
- ,
- }
- }
+
+
-
- An index pattern can match a single source, for example,
-
-
-
-
- filebeat-4-3-22
-
-
-
-
- , or
-
- multiple
-
- data sources,
-
-
+
+
-
-
- filebeat-*
-
-
-
-
- .
-
-
-
-
+
+ Create test index pattern
+
+
+
+ Beta
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
-
- Read documentation
-
-
-
-
-
-
-
-
-
-
-
- Test prompt
-
-
+
+
+ multiple
+ ,
+ "single":
+ filebeat-4-3-22
+ ,
+ "star":
+ filebeat-*
+ ,
+ }
+ }
+ >
+
+ An index pattern can match a single source, for example,
+
+
+
+
+ filebeat-4-3-22
+
+
+
+
+ , or
+
+ multiple
+
+ data sources,
+
+
+
+
+ filebeat-*
+
+
+
+
+ .
+
+
+
+
+
+
+
+ Read documentation
+
+
+
+
+
+
+
+
+
+
+
+ Test prompt
+
+
+
+
+
+
`;
@@ -146,100 +203,145 @@ exports[`Header should render normally 1`] = `
}
indexPatternName="test index pattern"
>
-
-
-
+
Create test index pattern
-
-
-
+ }
+ >
+
-
-
+ Create test index pattern
+
+ }
+ responsive={true}
>
-
-
- multiple
- ,
- "single":
- filebeat-4-3-22
- ,
- "star":
- filebeat-*
- ,
- }
- }
+
+
-
- An index pattern can match a single source, for example,
-
-
-
-
- filebeat-4-3-22
-
-
-
-
- , or
-
- multiple
-
- data sources,
-
-
+
+
-
-
- filebeat-*
-
-
-
-
- .
-
-
-
-
+
+ Create test index pattern
+
+
+
+
+
+
+
-
-
+
+
+
+
-
- Read documentation
-
-
-
-
-
-
-
-
+
+
+ multiple
+ ,
+ "single":
+ filebeat-4-3-22
+ ,
+ "star":
+ filebeat-*
+ ,
+ }
+ }
+ >
+
+ An index pattern can match a single source, for example,
+
+
+
+
+ filebeat-4-3-22
+
+
+
+
+ , or
+
+ multiple
+
+ data sources,
+
+
+
+
+ filebeat-*
+
+
+
+
+ .
+
+
+
+
+
+
+
+ Read documentation
+
+
+
+
+
+
+
+
+
+
+
+
`;
@@ -254,99 +356,144 @@ exports[`Header should render without including system indices 1`] = `
}
indexPatternName="test index pattern"
>
-
-
-
+
Create test index pattern
-
-
-
+ }
+ >
+
-
-
+ Create test index pattern
+
+ }
+ responsive={true}
>
-
-
- multiple
- ,
- "single":
- filebeat-4-3-22
- ,
- "star":
- filebeat-*
- ,
- }
- }
+
+
-
- An index pattern can match a single source, for example,
-
-
-
-
- filebeat-4-3-22
-
-
-
-
- , or
-
- multiple
-
- data sources,
-
-
+
+
-
-
- filebeat-*
-
-
-
-
- .
-
-
-
-
+
+ Create test index pattern
+
+
+
+
+
+
+
-
-
+
+
+
+
-
- Read documentation
-
-
-
-
-
-
-
-
+
+
+ multiple
+ ,
+ "single":
+ filebeat-4-3-22
+ ,
+ "star":
+ filebeat-*
+ ,
+ }
+ }
+ >
+
+ An index pattern can match a single source, for example,
+
+
+
+
+ filebeat-4-3-22
+
+
+
+
+ , or
+
+ multiple
+
+ data sources,
+
+
+
+
+ filebeat-*
+
+
+
+
+ .
+
+
+
+
+
+
+
+ Read documentation
+
+
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx
index a7e3b2ded75dc..c708bd3cac33e 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx
@@ -8,7 +8,7 @@
import React from 'react';
-import { EuiBetaBadge, EuiSpacer, EuiTitle, EuiText, EuiCode, EuiLink } from '@elastic/eui';
+import { EuiBetaBadge, EuiCode, EuiLink, EuiPageHeader, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -39,9 +39,9 @@ export const Header = ({
changeTitle(createIndexPatternHeader);
return (
-
-
-
+
{createIndexPatternHeader}
{isBeta ? (
<>
@@ -53,9 +53,10 @@ export const Header = ({
/>
>
) : null}
-
-
-
+ >
+ }
+ bottomBorder
+ >
) : null}
-
+
);
};
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx
index 633906feb785b..5bc53105dbcf8 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx
@@ -6,17 +6,12 @@
* Side Public License, v 1.
*/
-import React, { ReactElement, Component } from 'react';
-
-import {
- EuiGlobalToastList,
- EuiGlobalToastListToast,
- EuiPageContent,
- EuiHorizontalRule,
-} from '@elastic/eui';
+import React, { Component, ReactElement } from 'react';
+
+import { EuiGlobalToastList, EuiGlobalToastListToast, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
import { DocLinksStart } from 'src/core/public';
import { StepIndexPattern } from './components/step_index_pattern';
import { StepTimeField } from './components/step_time_field';
@@ -227,9 +222,9 @@ export class CreateIndexPatternWizard extends Component<
const initialQuery = new URLSearchParams(location.search).get('id') || undefined;
return (
-
+ <>
{header}
-
+
-
+ >
);
}
if (step === 2) {
return (
-
+ <>
{header}
-
+
-
+ >
);
}
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx
index 5aa9853c5e766..0c0adc6dd5029 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx
@@ -7,15 +7,15 @@
*/
import React from 'react';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
-import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
+import { EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IndexPattern, IndexPatternField } from '../../../../../../plugins/data/public';
import { useKibana } from '../../../../../../plugins/kibana_react/public';
import { IndexPatternManagmentContext } from '../../../types';
import { IndexHeader } from '../index_header';
-import { TAB_SCRIPTED_FIELDS, TAB_INDEXED_FIELDS } from '../constants';
+import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS } from '../constants';
import { FieldEditor } from '../../field_editor';
@@ -76,26 +76,18 @@ export const CreateEditField = withRouter(
if (spec) {
return (
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+ >
);
} else {
return <>>;
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx
index e314c00bc8176..6609605da87d1 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx
@@ -17,7 +17,6 @@ import {
EuiText,
EuiLink,
EuiCallOut,
- EuiPanel,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -145,15 +144,13 @@ export const EditIndexPattern = withRouter(
const kibana = useKibana();
const docsUrl = kibana.services.docLinks!.links.elasticsearch.mapping;
return (
-
-
-
-
+
+
{showTagsSection && (
{Boolean(indexPattern.timeFieldName) && (
@@ -193,19 +190,19 @@ export const EditIndexPattern = withRouter(
>
)}
-
- {
- setFields(indexPattern.getNonScriptedFields());
- }}
- />
-
-
+
+
+
{
+ setFields(indexPattern.getNonScriptedFields());
+ }}
+ />
+
);
}
);
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx
index 482cd574c8f1d..c141c228a68f2 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiFlexGroup, EuiToolTip, EuiFlexItem, EuiTitle, EuiButtonIcon } from '@elastic/eui';
+import { EuiButtonIcon, EuiPageHeader, EuiToolTip } from '@elastic/eui';
import { IIndexPattern } from 'src/plugins/data/public';
interface IndexHeaderProps {
@@ -40,50 +40,42 @@ const removeTooltip = i18n.translate('indexPatternManagement.editIndexPattern.re
defaultMessage: 'Remove index pattern.',
});
-export function IndexHeader({
+export const IndexHeader: React.FC = ({
defaultIndex,
indexPattern,
setDefault,
deleteIndexPatternClick,
-}: IndexHeaderProps) {
+ children,
+}) => {
return (
-
-
-
- {indexPattern.title}
-
-
-
-
- {defaultIndex !== indexPattern.id && setDefault && (
-
-
-
-
-
- )}
-
- {deleteIndexPatternClick && (
-
-
-
-
-
- )}
-
-
-
+ {indexPattern.title}}
+ rightSideItems={[
+ defaultIndex !== indexPattern.id && setDefault && (
+
+
+
+ ),
+ deleteIndexPatternClick && (
+
+
+
+ ),
+ ].filter(Boolean)}
+ >
+ {children}
+
);
-}
+};
diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap
index c5e6d1220d8bf..bc69fa29e6904 100644
--- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap
@@ -3,9 +3,11 @@
exports[`EmptyIndexPatternPrompt should render normally 1`] = `
diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap
index 1310488c65fab..957c94c80680d 100644
--- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap
@@ -4,9 +4,11 @@ exports[`EmptyState should render normally 1`] = `
diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx
index 240e732752916..c05f6a1f193b7 100644
--- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx
+++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx
@@ -63,8 +63,10 @@ export const EmptyState = ({
diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
index f018294f27c84..6bd06528084ce 100644
--- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
+++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
@@ -8,24 +8,20 @@
import {
EuiBadge,
+ EuiBadgeGroup,
EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
EuiInMemoryTable,
+ EuiPageHeader,
EuiSpacer,
- EuiText,
- EuiBadgeGroup,
- EuiPageContent,
- EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
-import React, { useState, useEffect } from 'react';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { reactRouterNavigate, useKibana } from '../../../../../plugins/kibana_react/public';
import { IndexPatternManagmentContext } from '../../types';
import { CreateButton } from '../create_button';
-import { IndexPatternTableItem, IndexPatternCreationOption } from '../types';
+import { IndexPatternCreationOption, IndexPatternTableItem } from '../types';
import { getIndexPatterns } from '../utils';
import { getListBreadcrumbs } from '../breadcrumbs';
import { EmptyState } from './empty_state';
@@ -54,10 +50,6 @@ const search = {
},
};
-const ariaRegion = i18n.translate('indexPatternManagement.editIndexPatternLiveRegionAriaLabel', {
- defaultMessage: 'Index patterns',
-});
-
const title = i18n.translate('indexPatternManagement.indexPatternTable.title', {
defaultMessage: 'Index patterns',
});
@@ -197,25 +189,21 @@ export const IndexPatternTable = ({ canSave, history }: Props) => {
}
return (
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
- {createButton}
-
-
+
+
+ }
+ bottomBorder
+ rightSideItems={[createButton]}
+ />
+
+
+
{
sorting={sorting}
search={search}
/>
-
+
);
};
diff --git a/src/plugins/newsfeed/kibana.json b/src/plugins/newsfeed/kibana.json
index b9f37b67f6921..0e7ae7cd11c35 100644
--- a/src/plugins/newsfeed/kibana.json
+++ b/src/plugins/newsfeed/kibana.json
@@ -2,5 +2,6 @@
"id": "newsfeed",
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "requiredPlugins": ["screenshotMode"]
}
diff --git a/src/plugins/newsfeed/public/lib/api.test.mocks.ts b/src/plugins/newsfeed/public/lib/api.test.mocks.ts
index 677bc203cbef3..8ac66eae6c2f6 100644
--- a/src/plugins/newsfeed/public/lib/api.test.mocks.ts
+++ b/src/plugins/newsfeed/public/lib/api.test.mocks.ts
@@ -8,6 +8,7 @@
import { storageMock } from './storage.mock';
import { driverMock } from './driver.mock';
+import { NeverFetchNewsfeedApiDriver } from './never_fetch_driver';
export const storageInstanceMock = storageMock.create();
jest.doMock('./storage', () => ({
@@ -18,3 +19,7 @@ export const driverInstanceMock = driverMock.create();
jest.doMock('./driver', () => ({
NewsfeedApiDriver: jest.fn().mockImplementation(() => driverInstanceMock),
}));
+
+jest.doMock('./never_fetch_driver', () => ({
+ NeverFetchNewsfeedApiDriver: jest.fn(() => new NeverFetchNewsfeedApiDriver()),
+}));
diff --git a/src/plugins/newsfeed/public/lib/api.test.ts b/src/plugins/newsfeed/public/lib/api.test.ts
index a4894573932e6..58d06e72cd77c 100644
--- a/src/plugins/newsfeed/public/lib/api.test.ts
+++ b/src/plugins/newsfeed/public/lib/api.test.ts
@@ -7,12 +7,16 @@
*/
import { driverInstanceMock, storageInstanceMock } from './api.test.mocks';
+
import moment from 'moment';
import { getApi } from './api';
import { TestScheduler } from 'rxjs/testing';
import { FetchResult, NewsfeedPluginBrowserConfig } from '../types';
import { take } from 'rxjs/operators';
+import { NewsfeedApiDriver as MockNewsfeedApiDriver } from './driver';
+import { NeverFetchNewsfeedApiDriver as MockNeverFetchNewsfeedApiDriver } from './never_fetch_driver';
+
const kibanaVersion = '8.0.0';
const newsfeedId = 'test';
@@ -46,6 +50,8 @@ describe('getApi', () => {
afterEach(() => {
storageInstanceMock.isAnyUnread$.mockReset();
driverInstanceMock.fetchNewsfeedItems.mockReset();
+ (MockNewsfeedApiDriver as jest.Mock).mockClear();
+ (MockNeverFetchNewsfeedApiDriver as jest.Mock).mockClear();
});
it('merges the newsfeed and unread observables', () => {
@@ -60,7 +66,7 @@ describe('getApi', () => {
a: createFetchResult({ feedItems: ['item' as any] }),
})
);
- const api = getApi(createConfig(1000), kibanaVersion, newsfeedId);
+ const api = getApi(createConfig(1000), kibanaVersion, newsfeedId, false);
expectObservable(api.fetchResults$.pipe(take(1))).toBe('(a|)', {
a: createFetchResult({
@@ -83,7 +89,7 @@ describe('getApi', () => {
a: createFetchResult({ feedItems: ['item' as any] }),
})
);
- const api = getApi(createConfig(2), kibanaVersion, newsfeedId);
+ const api = getApi(createConfig(2), kibanaVersion, newsfeedId, false);
expectObservable(api.fetchResults$.pipe(take(2))).toBe('a-(b|)', {
a: createFetchResult({
@@ -111,7 +117,7 @@ describe('getApi', () => {
a: createFetchResult({}),
})
);
- const api = getApi(createConfig(10), kibanaVersion, newsfeedId);
+ const api = getApi(createConfig(10), kibanaVersion, newsfeedId, false);
expectObservable(api.fetchResults$.pipe(take(2))).toBe('a--(b|)', {
a: createFetchResult({
@@ -123,4 +129,16 @@ describe('getApi', () => {
});
});
});
+
+ it('uses the news feed API driver if in not screenshot mode', () => {
+ getApi(createConfig(10), kibanaVersion, newsfeedId, false);
+ expect(MockNewsfeedApiDriver).toHaveBeenCalled();
+ expect(MockNeverFetchNewsfeedApiDriver).not.toHaveBeenCalled();
+ });
+
+ it('uses the never fetch news feed API driver if in not screenshot mode', () => {
+ getApi(createConfig(10), kibanaVersion, newsfeedId, true);
+ expect(MockNewsfeedApiDriver).not.toHaveBeenCalled();
+ expect(MockNeverFetchNewsfeedApiDriver).toHaveBeenCalled();
+ });
});
diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts
index 4fbbd8687b73f..7aafc9fd27625 100644
--- a/src/plugins/newsfeed/public/lib/api.ts
+++ b/src/plugins/newsfeed/public/lib/api.ts
@@ -11,6 +11,7 @@ import { map, catchError, filter, mergeMap, tap } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { FetchResult, NewsfeedPluginBrowserConfig } from '../types';
import { NewsfeedApiDriver } from './driver';
+import { NeverFetchNewsfeedApiDriver } from './never_fetch_driver';
import { NewsfeedStorage } from './storage';
export enum NewsfeedApiEndpoint {
@@ -40,13 +41,23 @@ export interface NewsfeedApi {
export function getApi(
config: NewsfeedPluginBrowserConfig,
kibanaVersion: string,
- newsfeedId: string
+ newsfeedId: string,
+ isScreenshotMode: boolean
): NewsfeedApi {
- const userLanguage = i18n.getLocale();
- const fetchInterval = config.fetchInterval.asMilliseconds();
- const mainInterval = config.mainInterval.asMilliseconds();
const storage = new NewsfeedStorage(newsfeedId);
- const driver = new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval, storage);
+ const mainInterval = config.mainInterval.asMilliseconds();
+
+ const createNewsfeedApiDriver = () => {
+ if (isScreenshotMode) {
+ return new NeverFetchNewsfeedApiDriver();
+ }
+
+ const userLanguage = i18n.getLocale();
+ const fetchInterval = config.fetchInterval.asMilliseconds();
+ return new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval, storage);
+ };
+
+ const driver = createNewsfeedApiDriver();
const results$ = timer(0, mainInterval).pipe(
filter(() => driver.shouldFetch()),
diff --git a/src/plugins/newsfeed/public/lib/driver.ts b/src/plugins/newsfeed/public/lib/driver.ts
index 0efa981e8c89d..1762c4a428784 100644
--- a/src/plugins/newsfeed/public/lib/driver.ts
+++ b/src/plugins/newsfeed/public/lib/driver.ts
@@ -10,6 +10,7 @@ import moment from 'moment';
import * as Rx from 'rxjs';
import { NEWSFEED_DEFAULT_SERVICE_BASE_URL } from '../../common/constants';
import { ApiItem, FetchResult, NewsfeedPluginBrowserConfig } from '../types';
+import { INewsfeedApiDriver } from './types';
import { convertItems } from './convert_items';
import type { NewsfeedStorage } from './storage';
@@ -19,7 +20,7 @@ interface NewsfeedResponse {
items: ApiItem[];
}
-export class NewsfeedApiDriver {
+export class NewsfeedApiDriver implements INewsfeedApiDriver {
private readonly kibanaVersion: string;
private readonly loadedTime = moment().utc(); // the date is compared to time in UTC format coming from the service
diff --git a/src/plugins/newsfeed/public/lib/never_fetch_driver.ts b/src/plugins/newsfeed/public/lib/never_fetch_driver.ts
new file mode 100644
index 0000000000000..e95ca9c2d499a
--- /dev/null
+++ b/src/plugins/newsfeed/public/lib/never_fetch_driver.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Observable } from 'rxjs';
+import { FetchResult } from '../types';
+import { INewsfeedApiDriver } from './types';
+
+/**
+ * NewsfeedApiDriver variant that never fetches results. This is useful for instances where Kibana is started
+ * without any user interaction like when generating a PDF or PNG report.
+ */
+export class NeverFetchNewsfeedApiDriver implements INewsfeedApiDriver {
+ shouldFetch(): boolean {
+ return false;
+ }
+
+ fetchNewsfeedItems(): Observable {
+ throw new Error('Not implemented!');
+ }
+}
diff --git a/src/plugins/newsfeed/public/lib/types.ts b/src/plugins/newsfeed/public/lib/types.ts
new file mode 100644
index 0000000000000..5a62a929eeb7f
--- /dev/null
+++ b/src/plugins/newsfeed/public/lib/types.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Observable } from 'rxjs';
+import type { FetchResult, NewsfeedPluginBrowserConfig } from '../types';
+
+export interface INewsfeedApiDriver {
+ /**
+ * Check whether newsfeed items should be (re-)fetched
+ */
+ shouldFetch(): boolean;
+
+ fetchNewsfeedItems(config: NewsfeedPluginBrowserConfig['service']): Observable;
+}
diff --git a/src/plugins/newsfeed/public/plugin.test.ts b/src/plugins/newsfeed/public/plugin.test.ts
new file mode 100644
index 0000000000000..4be69feb79f55
--- /dev/null
+++ b/src/plugins/newsfeed/public/plugin.test.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { take } from 'rxjs/operators';
+import { coreMock } from '../../../core/public/mocks';
+import { NewsfeedPublicPlugin } from './plugin';
+import { NewsfeedApiEndpoint } from './lib/api';
+
+describe('Newsfeed plugin', () => {
+ let plugin: NewsfeedPublicPlugin;
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ beforeEach(() => {
+ plugin = new NewsfeedPublicPlugin(coreMock.createPluginInitializerContext());
+ });
+
+ describe('#start', () => {
+ beforeEach(() => {
+ plugin.setup(coreMock.createSetup());
+ });
+
+ beforeEach(() => {
+ /**
+ * We assume for these tests that the newsfeed stream exposed by start will fetch newsfeed items
+ * on the first tick for new subscribers
+ */
+ jest.spyOn(window, 'fetch');
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('base case', () => {
+ it('makes fetch requests', () => {
+ const startContract = plugin.start(coreMock.createStart(), {
+ screenshotMode: { isScreenshotMode: () => false },
+ });
+ const sub = startContract
+ .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do
+ .pipe(take(1))
+ .subscribe(() => {});
+ jest.runOnlyPendingTimers();
+ expect(window.fetch).toHaveBeenCalled();
+ sub.unsubscribe();
+ });
+ });
+
+ describe('when in screenshot mode', () => {
+ it('makes no fetch requests in screenshot mode', () => {
+ const startContract = plugin.start(coreMock.createStart(), {
+ screenshotMode: { isScreenshotMode: () => true },
+ });
+ const sub = startContract
+ .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do
+ .pipe(take(1))
+ .subscribe(() => {});
+ jest.runOnlyPendingTimers();
+ expect(window.fetch).not.toHaveBeenCalled();
+ sub.unsubscribe();
+ });
+ });
+ });
+});
diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx
index fdda0a24b8bd5..656fc2ef00bb9 100644
--- a/src/plugins/newsfeed/public/plugin.tsx
+++ b/src/plugins/newsfeed/public/plugin.tsx
@@ -13,7 +13,7 @@ import React from 'react';
import moment from 'moment';
import { I18nProvider } from '@kbn/i18n/react';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
-import { NewsfeedPluginBrowserConfig } from './types';
+import { NewsfeedPluginBrowserConfig, NewsfeedPluginStartDependencies } from './types';
import { NewsfeedNavButton } from './components/newsfeed_header_nav_button';
import { getApi, NewsfeedApi, NewsfeedApiEndpoint } from './lib/api';
@@ -41,8 +41,10 @@ export class NewsfeedPublicPlugin
return {};
}
- public start(core: CoreStart) {
- const api = this.createNewsfeedApi(this.config, NewsfeedApiEndpoint.KIBANA);
+ public start(core: CoreStart, { screenshotMode }: NewsfeedPluginStartDependencies) {
+ const isScreenshotMode = screenshotMode.isScreenshotMode();
+
+ const api = this.createNewsfeedApi(this.config, NewsfeedApiEndpoint.KIBANA, isScreenshotMode);
core.chrome.navControls.registerRight({
order: 1000,
mount: (target) => this.mount(api, target),
@@ -56,7 +58,7 @@ export class NewsfeedPublicPlugin
pathTemplate: `/${endpoint}/v{VERSION}.json`,
},
});
- const { fetchResults$ } = this.createNewsfeedApi(config, endpoint);
+ const { fetchResults$ } = this.createNewsfeedApi(config, endpoint, isScreenshotMode);
return fetchResults$;
},
};
@@ -68,9 +70,10 @@ export class NewsfeedPublicPlugin
private createNewsfeedApi(
config: NewsfeedPluginBrowserConfig,
- newsfeedId: NewsfeedApiEndpoint
+ newsfeedId: NewsfeedApiEndpoint,
+ isScreenshotMode: boolean
): NewsfeedApi {
- const api = getApi(config, this.kibanaVersion, newsfeedId);
+ const api = getApi(config, this.kibanaVersion, newsfeedId, isScreenshotMode);
return {
markAsRead: api.markAsRead,
fetchResults$: api.fetchResults$.pipe(
diff --git a/src/plugins/newsfeed/public/types.ts b/src/plugins/newsfeed/public/types.ts
index cca656565f4ca..a7ff917f6f975 100644
--- a/src/plugins/newsfeed/public/types.ts
+++ b/src/plugins/newsfeed/public/types.ts
@@ -7,6 +7,10 @@
*/
import { Duration, Moment } from 'moment';
+import type { ScreenshotModePluginStart } from 'src/plugins/screenshot_mode/public';
+export interface NewsfeedPluginStartDependencies {
+ screenshotMode: ScreenshotModePluginStart;
+}
// Ideally, we may want to obtain the type from the configSchema and exposeToBrowser keys...
export interface NewsfeedPluginBrowserConfig {
diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json
index 66244a22336c7..18e6f2de1bc6f 100644
--- a/src/plugins/newsfeed/tsconfig.json
+++ b/src/plugins/newsfeed/tsconfig.json
@@ -7,13 +7,9 @@
"declaration": true,
"declarationMap": true
},
- "include": [
- "public/**/*",
- "server/**/*",
- "common/*",
- "../../../typings/**/*"
- ],
+ "include": ["public/**/*", "server/**/*", "common/*", "../../../typings/**/*"],
"references": [
- { "path": "../../core/tsconfig.json" }
+ { "path": "../../core/tsconfig.json" },
+ { "path": "../screenshot_mode/tsconfig.json" }
]
}
diff --git a/src/plugins/presentation_util/public/mocks.ts b/src/plugins/presentation_util/public/mocks.ts
new file mode 100644
index 0000000000000..91c461646c280
--- /dev/null
+++ b/src/plugins/presentation_util/public/mocks.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { CoreStart } from 'kibana/public';
+import { PresentationUtilPluginStart } from './types';
+import { pluginServices } from './services';
+import { registry } from './services/kibana';
+
+const createStartContract = (coreStart: CoreStart): PresentationUtilPluginStart => {
+ pluginServices.setRegistry(registry.start({ coreStart, startPlugins: {} as any }));
+
+ const startContract: PresentationUtilPluginStart = {
+ ContextProvider: pluginServices.getContextProvider(),
+ labsService: pluginServices.getServices().labs,
+ };
+ return startContract;
+};
+
+export const presentationUtilPluginMock = {
+ createStartContract,
+};
diff --git a/src/plugins/saved_objects/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects/public/finder/saved_object_finder.tsx
index 8d5e89664212c..da65b5b9fdda8 100644
--- a/src/plugins/saved_objects/public/finder/saved_object_finder.tsx
+++ b/src/plugins/saved_objects/public/finder/saved_object_finder.tsx
@@ -46,6 +46,7 @@ export interface SavedObjectMetaData {
getIconForSavedObject(savedObject: SimpleSavedObject): IconType;
getTooltipForSavedObject?(savedObject: SimpleSavedObject): string;
showSavedObject?(savedObject: SimpleSavedObject): boolean;
+ getSavedObjectSubType?(savedObject: SimpleSavedObject): string;
includeFields?: string[];
}
diff --git a/src/plugins/screenshot_mode/public/index.ts b/src/plugins/screenshot_mode/public/index.ts
index a5ad37dd5b760..012f57e837f41 100644
--- a/src/plugins/screenshot_mode/public/index.ts
+++ b/src/plugins/screenshot_mode/public/index.ts
@@ -18,4 +18,4 @@ export {
KBN_SCREENSHOT_MODE_ENABLED_KEY,
} from '../common';
-export { ScreenshotModePluginSetup } from './types';
+export { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types';
diff --git a/src/plugins/screenshot_mode/public/plugin.test.ts b/src/plugins/screenshot_mode/public/plugin.test.ts
index 33ae501466876..f2c0970d0ff60 100644
--- a/src/plugins/screenshot_mode/public/plugin.test.ts
+++ b/src/plugins/screenshot_mode/public/plugin.test.ts
@@ -21,7 +21,7 @@ describe('Screenshot mode public', () => {
setScreenshotModeDisabled();
});
- describe('setup contract', () => {
+ describe('public contract', () => {
it('detects screenshot mode "true"', () => {
setScreenshotModeEnabled();
const screenshotMode = plugin.setup(coreMock.createSetup());
@@ -34,10 +34,4 @@ describe('Screenshot mode public', () => {
expect(screenshotMode.isScreenshotMode()).toBe(false);
});
});
-
- describe('start contract', () => {
- it('returns nothing', () => {
- expect(plugin.start(coreMock.createStart())).toBe(undefined);
- });
- });
});
diff --git a/src/plugins/screenshot_mode/public/plugin.ts b/src/plugins/screenshot_mode/public/plugin.ts
index 7a166566a0173..a005bb7c3d055 100644
--- a/src/plugins/screenshot_mode/public/plugin.ts
+++ b/src/plugins/screenshot_mode/public/plugin.ts
@@ -8,18 +8,22 @@
import { CoreSetup, CoreStart, Plugin } from '../../../core/public';
-import { ScreenshotModePluginSetup } from './types';
+import { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types';
import { getScreenshotMode } from '../common';
export class ScreenshotModePlugin implements Plugin {
+ private publicContract = Object.freeze({
+ isScreenshotMode: () => getScreenshotMode() === true,
+ });
+
public setup(core: CoreSetup): ScreenshotModePluginSetup {
- return {
- isScreenshotMode: () => getScreenshotMode() === true,
- };
+ return this.publicContract;
}
- public start(core: CoreStart) {}
+ public start(core: CoreStart): ScreenshotModePluginStart {
+ return this.publicContract;
+ }
public stop() {}
}
diff --git a/src/plugins/screenshot_mode/public/types.ts b/src/plugins/screenshot_mode/public/types.ts
index 744ea8615f2a7..f6963de0cbd63 100644
--- a/src/plugins/screenshot_mode/public/types.ts
+++ b/src/plugins/screenshot_mode/public/types.ts
@@ -15,3 +15,4 @@ export interface IScreenshotModeService {
}
export type ScreenshotModePluginSetup = IScreenshotModeService;
+export type ScreenshotModePluginStart = IScreenshotModeService;
diff --git a/src/plugins/security_oss/kibana.json b/src/plugins/security_oss/kibana.json
index 70e37d586f1db..c93b5c3b60714 100644
--- a/src/plugins/security_oss/kibana.json
+++ b/src/plugins/security_oss/kibana.json
@@ -1,5 +1,10 @@
{
"id": "securityOss",
+ "owner": {
+ "name": "Platform Security",
+ "githubTeam": "kibana-security"
+ },
+ "description": "This plugin exposes a limited set of security functionality to OSS plugins.",
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["security"],
diff --git a/src/plugins/spaces_oss/kibana.json b/src/plugins/spaces_oss/kibana.json
index e048fb7ffb79c..10127634618f1 100644
--- a/src/plugins/spaces_oss/kibana.json
+++ b/src/plugins/spaces_oss/kibana.json
@@ -1,5 +1,10 @@
{
"id": "spacesOss",
+ "owner": {
+ "name": "Platform Security",
+ "githubTeam": "kibana-security"
+ },
+ "description": "This plugin exposes a limited set of spaces functionality to OSS plugins.",
"version": "kibana",
"server": false,
"ui": true,
diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap
index 17a91a4d43cc7..cbfece0b081c6 100644
--- a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap
+++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap
@@ -5,6 +5,7 @@ Object {
"as": "tagloud_vis",
"type": "render",
"value": Object {
+ "syncColors": false,
"visData": Object {
"columns": Array [
Object {
@@ -20,6 +21,12 @@ Object {
"type": "datatable",
},
"visParams": Object {
+ "bucket": Object {
+ "accessor": 1,
+ "format": Object {
+ "id": "number",
+ },
+ },
"maxFontSize": 72,
"metric": Object {
"accessor": 0,
@@ -29,6 +36,10 @@ Object {
},
"minFontSize": 18,
"orientation": "single",
+ "palette": Object {
+ "name": "default",
+ "type": "palette",
+ },
"scale": "linear",
"showLabel": true,
},
diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap
index a8bc0b4c51678..fed6fb54288f2 100644
--- a/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap
+++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap
@@ -84,6 +84,9 @@ Object {
"orientation": Array [
"single",
],
+ "palette": Array [
+ "default",
+ ],
"scale": Array [
"linear",
],
diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap
deleted file mode 100644
index 88ed7c66a79a2..0000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap
+++ /dev/null
@@ -1,3 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`tag cloud tests tagcloudscreenshot should render simple image 1`] = `"foo bar foobar "`;
diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap
deleted file mode 100644
index d7707f64d8a4f..0000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap
+++ /dev/null
@@ -1,7 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`TagCloudVisualizationTest TagCloudVisualization - basics simple draw 1`] = `"CN IN US DE BR "`;
-
-exports[`TagCloudVisualizationTest TagCloudVisualization - basics with param change 1`] = `"CN IN US DE BR "`;
-
-exports[`TagCloudVisualizationTest TagCloudVisualization - basics with resize 1`] = `"CN IN US DE BR "`;
diff --git a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js b/src/plugins/vis_type_tagcloud/public/components/feedback_message.js
deleted file mode 100644
index 9e1d66b0a2faa..0000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import React, { Component, Fragment } from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiIconTip } from '@elastic/eui';
-
-export class FeedbackMessage extends Component {
- constructor() {
- super();
- this.state = { shouldShowTruncate: false, shouldShowIncomplete: false };
- }
-
- render() {
- if (!this.state.shouldShowTruncate && !this.state.shouldShowIncomplete) {
- return '';
- }
-
- return (
-
- {this.state.shouldShowTruncate && (
-
-
-
- )}
- {this.state.shouldShowIncomplete && (
-
-
-
- )}
-
- }
- />
- );
- }
-}
diff --git a/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx
new file mode 100644
index 0000000000000..82663bbf7070c
--- /dev/null
+++ b/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { lazy } from 'react';
+import { VisEditorOptionsProps } from 'src/plugins/visualizations/public';
+import { TagCloudVisParams, TagCloudTypeProps } from '../types';
+
+const TagCloudOptionsLazy = lazy(() => import('./tag_cloud_options'));
+
+export const getTagCloudOptions = ({ palettes }: TagCloudTypeProps) => (
+ props: VisEditorOptionsProps
+) => ;
diff --git a/src/plugins/vis_type_tagcloud/public/components/label.js b/src/plugins/vis_type_tagcloud/public/components/label.js
deleted file mode 100644
index 028a001cfbe63..0000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/label.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import React, { Component } from 'react';
-
-export class Label extends Component {
- constructor() {
- super();
- this.state = { label: '', shouldShowLabel: true };
- }
-
- render() {
- return (
-
- {this.state.label}
-
- );
- }
-}
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js
deleted file mode 100644
index 254d210eebf37..0000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js
+++ /dev/null
@@ -1,409 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import d3 from 'd3';
-import d3TagCloud from 'd3-cloud';
-import { EventEmitter } from 'events';
-
-const ORIENTATIONS = {
- single: () => 0,
- 'right angled': (tag) => {
- return hashWithinRange(tag.text, 2) * 90;
- },
- multiple: (tag) => {
- return hashWithinRange(tag.text, 12) * 15 - 90; //fan out 12 * 15 degrees over top-right and bottom-right quadrant (=-90 deg offset)
- },
-};
-const D3_SCALING_FUNCTIONS = {
- linear: () => d3.scale.linear(),
- log: () => d3.scale.log(),
- 'square root': () => d3.scale.sqrt(),
-};
-
-export class TagCloud extends EventEmitter {
- constructor(domNode, colorScale) {
- super();
-
- //DOM
- this._element = domNode;
- this._d3SvgContainer = d3.select(this._element).append('svg');
- this._svgGroup = this._d3SvgContainer.append('g');
- this._size = [1, 1];
- this.resize();
-
- //SETTING (non-configurable)
- /**
- * the fontFamily should be set explicitly for calculating a layout
- * and to avoid words overlapping
- */
- this._fontFamily = 'Inter UI, sans-serif';
- this._fontStyle = 'normal';
- this._fontWeight = 'normal';
- this._spiral = 'archimedean'; //layout shape
- this._timeInterval = 1000; //time allowed for layout algorithm
- this._padding = 5;
-
- //OPTIONS
- this._orientation = 'single';
- this._minFontSize = 10;
- this._maxFontSize = 36;
- this._textScale = 'linear';
- this._optionsAsString = null;
-
- //DATA
- this._words = null;
-
- //UTIL
- this._colorScale = colorScale;
- this._setTimeoutId = null;
- this._pendingJob = null;
- this._layoutIsUpdating = null;
- this._allInViewBox = false;
- this._DOMisUpdating = false;
- }
-
- setOptions(options) {
- if (JSON.stringify(options) === this._optionsAsString) {
- return;
- }
- this._optionsAsString = JSON.stringify(options);
- this._orientation = options.orientation;
- this._minFontSize = Math.min(options.minFontSize, options.maxFontSize);
- this._maxFontSize = Math.max(options.minFontSize, options.maxFontSize);
- this._textScale = options.scale;
- this._invalidate(false);
- }
-
- resize() {
- const newWidth = this._element.offsetWidth;
- const newHeight = this._element.offsetHeight;
-
- if (newWidth === this._size[0] && newHeight === this._size[1]) {
- return;
- }
-
- const wasInside = this._size[0] >= this._cloudWidth && this._size[1] >= this._cloudHeight;
- const willBeInside = this._cloudWidth <= newWidth && this._cloudHeight <= newHeight;
- this._size[0] = newWidth;
- this._size[1] = newHeight;
- if (wasInside && willBeInside && this._allInViewBox) {
- this._invalidate(true);
- } else {
- this._invalidate(false);
- }
- }
-
- setData(data) {
- this._words = data;
- this._invalidate(false);
- }
-
- destroy() {
- clearTimeout(this._setTimeoutId);
- this._element.innerHTML = '';
- }
-
- getStatus() {
- return this._allInViewBox ? TagCloud.STATUS.COMPLETE : TagCloud.STATUS.INCOMPLETE;
- }
-
- _updateContainerSize() {
- this._d3SvgContainer.attr('width', this._size[0]);
- this._d3SvgContainer.attr('height', this._size[1]);
- this._svgGroup.attr('width', this._size[0]);
- this._svgGroup.attr('height', this._size[1]);
- }
-
- _isJobRunning() {
- return this._setTimeoutId || this._layoutIsUpdating || this._DOMisUpdating;
- }
-
- async _processPendingJob() {
- if (!this._pendingJob) {
- return;
- }
-
- if (this._isJobRunning()) {
- return;
- }
-
- this._completedJob = null;
- const job = await this._pickPendingJob();
- if (job.words.length) {
- if (job.refreshLayout) {
- await this._updateLayout(job);
- }
- await this._updateDOM(job);
- const cloudBBox = this._svgGroup[0][0].getBBox();
- this._cloudWidth = cloudBBox.width;
- this._cloudHeight = cloudBBox.height;
- this._allInViewBox =
- cloudBBox.x >= 0 &&
- cloudBBox.y >= 0 &&
- cloudBBox.x + cloudBBox.width <= this._element.offsetWidth &&
- cloudBBox.y + cloudBBox.height <= this._element.offsetHeight;
- } else {
- this._emptyDOM(job);
- }
-
- if (this._pendingJob) {
- this._processPendingJob(); //pick up next job
- } else {
- this._completedJob = job;
- this.emit('renderComplete');
- }
- }
-
- async _pickPendingJob() {
- return await new Promise((resolve) => {
- this._setTimeoutId = setTimeout(async () => {
- const job = this._pendingJob;
- this._pendingJob = null;
- this._setTimeoutId = null;
- resolve(job);
- }, 0);
- });
- }
-
- _emptyDOM() {
- this._svgGroup.selectAll('text').remove();
- this._cloudWidth = 0;
- this._cloudHeight = 0;
- this._allInViewBox = true;
- this._DOMisUpdating = false;
- }
-
- async _updateDOM(job) {
- const canSkipDomUpdate = this._pendingJob || this._setTimeoutId;
- if (canSkipDomUpdate) {
- this._DOMisUpdating = false;
- return;
- }
-
- this._DOMisUpdating = true;
- const affineTransform = positionWord.bind(
- null,
- this._element.offsetWidth / 2,
- this._element.offsetHeight / 2
- );
- const svgTextNodes = this._svgGroup.selectAll('text');
- const stage = svgTextNodes.data(job.words, getText);
-
- await new Promise((resolve) => {
- const enterSelection = stage.enter();
- const enteringTags = enterSelection.append('text');
- enteringTags.style('font-size', getSizeInPixels);
- enteringTags.style('font-style', this._fontStyle);
- enteringTags.style('font-weight', () => this._fontWeight);
- enteringTags.style('font-family', () => this._fontFamily);
- enteringTags.style('fill', this.getFill.bind(this));
- enteringTags.attr('text-anchor', () => 'middle');
- enteringTags.attr('transform', affineTransform);
- enteringTags.attr('data-test-subj', getDisplayText);
- enteringTags.text(getDisplayText);
-
- const self = this;
- enteringTags.on({
- click: function (event) {
- self.emit('select', event);
- },
- mouseover: function () {
- d3.select(this).style('cursor', 'pointer');
- },
- mouseout: function () {
- d3.select(this).style('cursor', 'default');
- },
- });
-
- const movingTags = stage.transition();
- movingTags.duration(600);
- movingTags.style('font-size', getSizeInPixels);
- movingTags.style('font-style', this._fontStyle);
- movingTags.style('font-weight', () => this._fontWeight);
- movingTags.style('font-family', () => this._fontFamily);
- movingTags.attr('transform', affineTransform);
-
- const exitingTags = stage.exit();
- const exitTransition = exitingTags.transition();
- exitTransition.duration(200);
- exitingTags.style('fill-opacity', 1e-6);
- exitingTags.attr('font-size', 1);
- exitingTags.remove();
-
- let exits = 0;
- let moves = 0;
- const resolveWhenDone = () => {
- if (exits === 0 && moves === 0) {
- this._DOMisUpdating = false;
- resolve(true);
- }
- };
- exitTransition.each(() => exits++);
- exitTransition.each('end', () => {
- exits--;
- resolveWhenDone();
- });
- movingTags.each(() => moves++);
- movingTags.each('end', () => {
- moves--;
- resolveWhenDone();
- });
- });
- }
-
- _makeTextSizeMapper() {
- const mapSizeToFontSize = D3_SCALING_FUNCTIONS[this._textScale]();
- const range =
- this._words.length === 1
- ? [this._maxFontSize, this._maxFontSize]
- : [this._minFontSize, this._maxFontSize];
- mapSizeToFontSize.range(range);
- if (this._words) {
- mapSizeToFontSize.domain(d3.extent(this._words, getValue));
- }
- return mapSizeToFontSize;
- }
-
- _makeNewJob() {
- return {
- refreshLayout: true,
- size: this._size.slice(),
- words: this._words,
- };
- }
-
- _makeJobPreservingLayout() {
- return {
- refreshLayout: false,
- size: this._size.slice(),
- words: this._completedJob.words.map((tag) => {
- return {
- x: tag.x,
- y: tag.y,
- rotate: tag.rotate,
- size: tag.size,
- rawText: tag.rawText || tag.text,
- displayText: tag.displayText,
- meta: tag.meta,
- };
- }),
- };
- }
-
- _invalidate(keepLayout) {
- if (!this._words) {
- return;
- }
-
- this._updateContainerSize();
-
- const canReuseLayout = keepLayout && !this._isJobRunning() && this._completedJob;
- this._pendingJob = canReuseLayout ? this._makeJobPreservingLayout() : this._makeNewJob();
- this._processPendingJob();
- }
-
- async _updateLayout(job) {
- if (job.size[0] <= 0 || job.size[1] <= 0) {
- // If either width or height isn't above 0 we don't relayout anything,
- // since the d3-cloud will be stuck in an infinite loop otherwise.
- return;
- }
-
- const mapSizeToFontSize = this._makeTextSizeMapper();
- const tagCloudLayoutGenerator = d3TagCloud();
- tagCloudLayoutGenerator.size(job.size);
- tagCloudLayoutGenerator.padding(this._padding);
- tagCloudLayoutGenerator.rotate(ORIENTATIONS[this._orientation]);
- tagCloudLayoutGenerator.font(this._fontFamily);
- tagCloudLayoutGenerator.fontStyle(this._fontStyle);
- tagCloudLayoutGenerator.fontWeight(this._fontWeight);
- tagCloudLayoutGenerator.fontSize((tag) => mapSizeToFontSize(tag.value));
- tagCloudLayoutGenerator.random(seed);
- tagCloudLayoutGenerator.spiral(this._spiral);
- tagCloudLayoutGenerator.words(job.words);
- tagCloudLayoutGenerator.text(getDisplayText);
- tagCloudLayoutGenerator.timeInterval(this._timeInterval);
-
- this._layoutIsUpdating = true;
- await new Promise((resolve) => {
- tagCloudLayoutGenerator.on('end', () => {
- this._layoutIsUpdating = false;
- resolve(true);
- });
- tagCloudLayoutGenerator.start();
- });
- }
-
- /**
- * Returns debug info. For debugging only.
- * @return {*}
- */
- getDebugInfo() {
- const debug = {};
- debug.positions = this._completedJob
- ? this._completedJob.words.map((tag) => {
- return {
- displayText: tag.displayText,
- rawText: tag.rawText || tag.text,
- x: tag.x,
- y: tag.y,
- rotate: tag.rotate,
- };
- })
- : [];
- debug.size = {
- width: this._size[0],
- height: this._size[1],
- };
- return debug;
- }
-
- getFill(tag) {
- return this._colorScale(tag.text);
- }
-}
-
-TagCloud.STATUS = { COMPLETE: 0, INCOMPLETE: 1 };
-
-function seed() {
- return 0.5; //constant seed (not random) to ensure constant layouts for identical data
-}
-
-function getText(word) {
- return word.rawText;
-}
-
-function getDisplayText(word) {
- return word.displayText;
-}
-
-function positionWord(xTranslate, yTranslate, word) {
- if (isNaN(word.x) || isNaN(word.y) || isNaN(word.rotate)) {
- //move off-screen
- return `translate(${xTranslate * 3}, ${yTranslate * 3})rotate(0)`;
- }
-
- return `translate(${word.x + xTranslate}, ${word.y + yTranslate})rotate(${word.rotate})`;
-}
-
-function getValue(tag) {
- return tag.value;
-}
-
-function getSizeInPixels(tag) {
- return `${tag.size}px`;
-}
-
-function hashWithinRange(str, max) {
- str = JSON.stringify(str);
- let hash = 0;
- for (const ch of str) {
- hash = (hash * 31 + ch.charCodeAt(0)) % max;
- }
- return Math.abs(hash) % max;
-}
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss
index 37867f1ed1c17..51b5e9dedd844 100644
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss
@@ -5,18 +5,14 @@
// tgcChart__legend--small
// tgcChart__legend-isLoading
-.tgcChart__container, .tgcChart__wrapper {
+.tgcChart__wrapper {
flex: 1 1 0;
display: flex;
+ flex-direction: column;
}
-.tgcChart {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow: hidden;
+.tgcChart__wrapper text {
+ cursor: pointer;
}
.tgcChart__label {
@@ -24,3 +20,7 @@
text-align: center;
font-weight: $euiFontWeightBold;
}
+
+.tgcChart__warning {
+ width: $euiSize;
+}
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js
deleted file mode 100644
index eb575457146c5..0000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js
+++ /dev/null
@@ -1,507 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import _ from 'lodash';
-import d3 from 'd3';
-import 'jest-canvas-mock';
-
-import { fromNode, delay } from 'bluebird';
-import { TagCloud } from './tag_cloud';
-import { setHTMLElementOffset, setSVGElementGetBBox } from '@kbn/test/jest';
-
-describe('tag cloud tests', () => {
- let SVGElementGetBBoxSpyInstance;
- let HTMLElementOffsetMockInstance;
-
- beforeEach(() => {
- setupDOM();
- });
-
- afterEach(() => {
- SVGElementGetBBoxSpyInstance.mockRestore();
- HTMLElementOffsetMockInstance.mockRestore();
- });
-
- const minValue = 1;
- const maxValue = 9;
- const midValue = (minValue + maxValue) / 2;
- const baseTest = {
- data: [
- { rawText: 'foo', displayText: 'foo', value: minValue },
- { rawText: 'bar', displayText: 'bar', value: midValue },
- { rawText: 'foobar', displayText: 'foobar', value: maxValue },
- ],
- options: {
- orientation: 'single',
- scale: 'linear',
- minFontSize: 10,
- maxFontSize: 36,
- },
- expected: [
- {
- text: 'foo',
- fontSize: '10px',
- },
- {
- text: 'bar',
- fontSize: '23px',
- },
- {
- text: 'foobar',
- fontSize: '36px',
- },
- ],
- };
-
- const singleLayoutTest = _.cloneDeep(baseTest);
-
- const rightAngleLayoutTest = _.cloneDeep(baseTest);
- rightAngleLayoutTest.options.orientation = 'right angled';
-
- const multiLayoutTest = _.cloneDeep(baseTest);
- multiLayoutTest.options.orientation = 'multiple';
-
- const mapWithLog = d3.scale.log();
- mapWithLog.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]);
- mapWithLog.domain([minValue, maxValue]);
- const logScaleTest = _.cloneDeep(baseTest);
- logScaleTest.options.scale = 'log';
- logScaleTest.expected[1].fontSize = Math.round(mapWithLog(midValue)) + 'px';
-
- const mapWithSqrt = d3.scale.sqrt();
- mapWithSqrt.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]);
- mapWithSqrt.domain([minValue, maxValue]);
- const sqrtScaleTest = _.cloneDeep(baseTest);
- sqrtScaleTest.options.scale = 'square root';
- sqrtScaleTest.expected[1].fontSize = Math.round(mapWithSqrt(midValue)) + 'px';
-
- const biggerFontTest = _.cloneDeep(baseTest);
- biggerFontTest.options.minFontSize = 36;
- biggerFontTest.options.maxFontSize = 72;
- biggerFontTest.expected[0].fontSize = '36px';
- biggerFontTest.expected[1].fontSize = '54px';
- biggerFontTest.expected[2].fontSize = '72px';
-
- const trimDataTest = _.cloneDeep(baseTest);
- trimDataTest.data.splice(1, 1);
- trimDataTest.expected.splice(1, 1);
-
- let domNode;
- let tagCloud;
-
- const colorScale = d3.scale
- .ordinal()
- .range(['#00a69b', '#57c17b', '#6f87d8', '#663db8', '#bc52bc', '#9e3533', '#daa05d']);
-
- function setupDOM() {
- domNode = document.createElement('div');
- SVGElementGetBBoxSpyInstance = setSVGElementGetBBox();
- HTMLElementOffsetMockInstance = setHTMLElementOffset(512, 512);
-
- document.body.appendChild(domNode);
- }
-
- function teardownDOM() {
- domNode.innerHTML = '';
- document.body.removeChild(domNode);
- }
-
- [
- singleLayoutTest,
- rightAngleLayoutTest,
- multiLayoutTest,
- logScaleTest,
- sqrtScaleTest,
- biggerFontTest,
- trimDataTest,
- ].forEach(function (currentTest) {
- describe(`should position elements correctly for options: ${JSON.stringify(
- currentTest.options
- )}`, () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(currentTest.data);
- tagCloud.setOptions(currentTest.options);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
-
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(currentTest.expected, textElements, tagCloud);
- })
- );
- });
- });
-
- [5, 100, 200, 300, 500].forEach((timeout) => {
- // FLAKY: https://github.com/elastic/kibana/issues/94043
- describe.skip(`should only send single renderComplete event at the very end, using ${timeout}ms timeout`, () => {
- beforeEach(async () => {
- //TagCloud takes at least 600ms to complete (due to d3 animation)
- //renderComplete should only notify at the last one
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
-
- //this timeout modifies the settings before the cloud is rendered.
- //the cloud needs to use the correct options
- setTimeout(() => tagCloud.setOptions(logScaleTest.options), timeout);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
-
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(logScaleTest.expected, textElements, tagCloud);
- })
- );
- });
- });
-
- describe('should use the latest state before notifying (when modifying options multiple times)', () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- tagCloud.setOptions(logScaleTest.options);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(logScaleTest.expected, textElements, tagCloud);
- })
- );
- });
-
- describe('should use the latest state before notifying (when modifying data multiple times)', () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- tagCloud.setData(trimDataTest.data);
-
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(trimDataTest.expected, textElements, tagCloud);
- })
- );
- });
-
- describe('should not get multiple render-events', () => {
- let counter;
- beforeEach(() => {
- counter = 0;
-
- return new Promise((resolve, reject) => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
-
- setTimeout(() => {
- //this should be overridden by later changes
- tagCloud.setData(sqrtScaleTest.data);
- tagCloud.setOptions(sqrtScaleTest.options);
- }, 100);
-
- setTimeout(() => {
- //latest change
- tagCloud.setData(logScaleTest.data);
- tagCloud.setOptions(logScaleTest.options);
- }, 300);
-
- tagCloud.on('renderComplete', function onRender() {
- if (counter > 0) {
- reject('Should not get multiple render events');
- }
- counter += 1;
- resolve(true);
- });
- });
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(logScaleTest.expected, textElements, tagCloud);
- })
- );
- });
-
- describe('should show correct data when state-updates are interleaved with resize event', () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(logScaleTest.data);
- tagCloud.setOptions(logScaleTest.options);
-
- await delay(1000); //let layout run
-
- SVGElementGetBBoxSpyInstance.mockRestore();
- SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(600, 600);
-
- tagCloud.resize(); //triggers new layout
- setTimeout(() => {
- //change the options at the very end too
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- }, 200);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(baseTest.expected, textElements, tagCloud);
- })
- );
- });
-
- describe(`should not put elements in view when container is too small`, () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test('completeness should not be ok', () => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE);
- });
- test('positions should not be ok', () => {
- const textElements = domNode.querySelectorAll('text');
- for (let i = 0; i < textElements; i++) {
- const bbox = textElements[i].getBoundingClientRect();
- verifyBbox(bbox, false, tagCloud);
- }
- });
- });
-
- describe(`tags should fit after making container bigger`, () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
-
- //make bigger
- tagCloud._size = [600, 600];
- tagCloud.resize();
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
- });
-
- describe(`tags should no longer fit after making container smaller`, () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
-
- //make smaller
- tagCloud._size = [];
- tagCloud.resize();
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test('completeness should not be ok', () => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE);
- });
- });
-
- describe('tagcloudscreenshot', () => {
- afterEach(teardownDOM);
-
- test('should render simple image', async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
-
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
-
- expect(domNode.innerHTML).toMatchSnapshot();
- });
- });
-
- function verifyTagProperties(expectedValues, actualElements, tagCloud) {
- expect(actualElements.length).toEqual(expectedValues.length);
- expectedValues.forEach((test, index) => {
- try {
- expect(actualElements[index].style.fontSize).toEqual(test.fontSize);
- } catch (e) {
- throw new Error('fontsize is not correct: ' + e.message);
- }
- try {
- expect(actualElements[index].innerHTML).toEqual(test.text);
- } catch (e) {
- throw new Error('fontsize is not correct: ' + e.message);
- }
- isInsideContainer(actualElements[index], tagCloud);
- });
- }
-
- function isInsideContainer(actualElement, tagCloud) {
- const bbox = actualElement.getBoundingClientRect();
- verifyBbox(bbox, true, tagCloud);
- }
-
- function verifyBbox(bbox, shouldBeInside, tagCloud) {
- const message = ` | bbox-of-tag: ${JSON.stringify([
- bbox.left,
- bbox.top,
- bbox.right,
- bbox.bottom,
- ])} vs
- bbox-of-container: ${domNode.offsetWidth},${domNode.offsetHeight}
- debugInfo: ${JSON.stringify(tagCloud.getDebugInfo())}`;
-
- try {
- expect(bbox.top >= 0 && bbox.top <= domNode.offsetHeight).toBe(shouldBeInside);
- } catch (e) {
- throw new Error(
- 'top boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message
- );
- }
- try {
- expect(bbox.bottom >= 0 && bbox.bottom <= domNode.offsetHeight).toBe(shouldBeInside);
- } catch (e) {
- throw new Error(
- 'bottom boundary of tag should have been ' +
- (shouldBeInside ? 'inside' : 'outside') +
- message
- );
- }
- try {
- expect(bbox.left >= 0 && bbox.left <= domNode.offsetWidth).toBe(shouldBeInside);
- } catch (e) {
- throw new Error(
- 'left boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message
- );
- }
- try {
- expect(bbox.right >= 0 && bbox.right <= domNode.offsetWidth).toBe(shouldBeInside);
- } catch (e) {
- throw new Error(
- 'right boundary of tag should have been ' +
- (shouldBeInside ? 'inside' : 'outside') +
- message
- );
- }
- }
-
- /**
- * In CI, this entire suite "blips" about 1/5 times.
- * This blip causes the majority of these tests fail for the exact same reason: One tag is centered inside the container,
- * while the others are moved out.
- * This has not been reproduced locally yet.
- * It may be an issue with the 3rd party d3-cloud that snags.
- *
- * The test suite should continue to catch reliably catch regressions of other sorts: unexpected and other uncaught errors,
- * scaling issues, ordering issues
- *
- */
- function shouldAssert() {
- const debugInfo = tagCloud.getDebugInfo();
- const count = debugInfo.positions.length;
- const largest = debugInfo.positions.pop(); //test suite puts largest tag at the end.
-
- const centered = largest[1] === 0 && largest[2] === 0;
- const halfWidth = debugInfo.size.width / 2;
- const halfHeight = debugInfo.size.height / 2;
- const inside = debugInfo.positions.filter((position) => {
- const x = position.x + halfWidth;
- const y = position.y + halfHeight;
- return 0 <= x && x <= debugInfo.size.width && 0 <= y && y <= debugInfo.size.height;
- });
-
- return centered && inside.length === count - 1;
- }
-
- function handleExpectedBlip(assertion) {
- return () => {
- if (!shouldAssert()) {
- return;
- }
- assertion();
- };
- }
-});
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx
new file mode 100644
index 0000000000000..b4d4e70d5ffe3
--- /dev/null
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import React from 'react';
+import { Wordcloud, Settings } from '@elastic/charts';
+import { chartPluginMock } from '../../../charts/public/mocks';
+import type { Datatable } from '../../../expressions/public';
+import { mount } from 'enzyme';
+import { findTestSubject } from '@elastic/eui/lib/test';
+import TagCloudChart, { TagCloudChartProps } from './tag_cloud_chart';
+import { TagCloudVisParams } from '../types';
+
+jest.mock('../services', () => ({
+ getFormatService: jest.fn(() => {
+ return {
+ deserialize: jest.fn(),
+ };
+ }),
+}));
+
+const palettesRegistry = chartPluginMock.createPaletteRegistry();
+const visData = ({
+ columns: [
+ {
+ id: 'col-0',
+ name: 'geo.dest: Descending',
+ },
+ {
+ id: 'col-1',
+ name: 'Count',
+ },
+ ],
+ rows: [
+ { 'col-0': 'CN', 'col-1': 26 },
+ { 'col-0': 'IN', 'col-1': 17 },
+ { 'col-0': 'US', 'col-1': 6 },
+ { 'col-0': 'DE', 'col-1': 4 },
+ { 'col-0': 'BR', 'col-1': 3 },
+ ],
+} as unknown) as Datatable;
+
+const visParams = {
+ bucket: { accessor: 0, format: {} },
+ metric: { accessor: 1, format: {} },
+ scale: 'linear',
+ orientation: 'single',
+ palette: {
+ type: 'palette',
+ name: 'default',
+ },
+ minFontSize: 12,
+ maxFontSize: 70,
+ showLabel: true,
+} as TagCloudVisParams;
+
+describe('TagCloudChart', function () {
+ let wrapperProps: TagCloudChartProps;
+
+ beforeAll(() => {
+ wrapperProps = {
+ visData,
+ visParams,
+ palettesRegistry,
+ fireEvent: jest.fn(),
+ renderComplete: jest.fn(),
+ syncColors: false,
+ visType: 'tagcloud',
+ };
+ });
+
+ it('renders the Wordcloud component', async () => {
+ const component = mount( );
+ expect(component.find(Wordcloud).length).toBe(1);
+ });
+
+ it('renders the label correctly', async () => {
+ const component = mount( );
+ const label = findTestSubject(component, 'tagCloudLabel');
+ expect(label.text()).toEqual('geo.dest: Descending - Count');
+ });
+
+ it('not renders the label if showLabel setting is off', async () => {
+ const newVisParams = { ...visParams, showLabel: false };
+ const newProps = { ...wrapperProps, visParams: newVisParams };
+ const component = mount( );
+ const label = findTestSubject(component, 'tagCloudLabel');
+ expect(label.length).toBe(0);
+ });
+
+ it('receives the data on the correct format', () => {
+ const component = mount( );
+ expect(component.find(Wordcloud).prop('data')).toStrictEqual([
+ {
+ color: 'black',
+ text: 'CN',
+ weight: 1,
+ },
+ {
+ color: 'black',
+ text: 'IN',
+ weight: 0.6086956521739131,
+ },
+ {
+ color: 'black',
+ text: 'US',
+ weight: 0.13043478260869565,
+ },
+ {
+ color: 'black',
+ text: 'DE',
+ weight: 0.043478260869565216,
+ },
+ {
+ color: 'black',
+ text: 'BR',
+ weight: 0,
+ },
+ ]);
+ });
+
+ it('sets the angles correctly', async () => {
+ const newVisParams = { ...visParams, orientation: 'right angled' } as TagCloudVisParams;
+ const newProps = { ...wrapperProps, visParams: newVisParams };
+ const component = mount( );
+ expect(component.find(Wordcloud).prop('endAngle')).toBe(90);
+ expect(component.find(Wordcloud).prop('angleCount')).toBe(2);
+ });
+
+ it('calls filter callback', () => {
+ const component = mount( );
+ component.find(Settings).prop('onElementClick')!([
+ [
+ {
+ text: 'BR',
+ weight: 0.17391304347826086,
+ color: '#d36086',
+ },
+ {
+ specId: 'tagCloud',
+ key: 'tagCloud',
+ },
+ ],
+ ]);
+ expect(wrapperProps.fireEvent).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx
index f668e22815b60..b89fe2fa90ede 100644
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx
@@ -6,64 +6,225 @@
* Side Public License, v 1.
*/
-import React, { useEffect, useMemo, useRef } from 'react';
-import { EuiResizeObserver } from '@elastic/eui';
+import React, { useCallback, useState, useMemo } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
import { throttle } from 'lodash';
-
-import { TagCloudVisDependencies } from '../plugin';
+import { EuiIconTip, EuiResizeObserver } from '@elastic/eui';
+import { Chart, Settings, Wordcloud, RenderChangeListener } from '@elastic/charts';
+import type { PaletteRegistry } from '../../../charts/public';
+import type { IInterpreterRenderHandlers } from '../../../expressions/public';
+import { getFormatService } from '../services';
import { TagCloudVisRenderValue } from '../tag_cloud_fn';
-// @ts-ignore
-import { TagCloudVisualization } from './tag_cloud_visualization';
import './tag_cloud.scss';
-type TagCloudChartProps = TagCloudVisDependencies &
- TagCloudVisRenderValue & {
- fireEvent: (event: any) => void;
- renderComplete: () => void;
- };
+const MAX_TAG_COUNT = 200;
+
+export type TagCloudChartProps = TagCloudVisRenderValue & {
+ fireEvent: IInterpreterRenderHandlers['event'];
+ renderComplete: IInterpreterRenderHandlers['done'];
+ palettesRegistry: PaletteRegistry;
+};
+
+const calculateWeight = (value: number, x1: number, y1: number, x2: number, y2: number) =>
+ ((value - x1) * (y2 - x2)) / (y1 - x1) + x2;
+
+const getColor = (
+ palettes: PaletteRegistry,
+ activePalette: string,
+ text: string,
+ values: string[],
+ syncColors: boolean
+) => {
+ return palettes?.get(activePalette).getCategoricalColor(
+ [
+ {
+ name: text,
+ rankAtDepth: values.length ? values.findIndex((name) => name === text) : 0,
+ totalSeriesAtDepth: values.length || 1,
+ },
+ ],
+ {
+ maxDepth: 1,
+ totalSeries: values.length || 1,
+ behindText: false,
+ syncColors,
+ }
+ );
+};
+
+const ORIENTATIONS = {
+ single: {
+ endAngle: 0,
+ angleCount: 360,
+ },
+ 'right angled': {
+ endAngle: 90,
+ angleCount: 2,
+ },
+ multiple: {
+ endAngle: -90,
+ angleCount: 12,
+ },
+};
export const TagCloudChart = ({
- colors,
visData,
visParams,
+ palettesRegistry,
fireEvent,
renderComplete,
+ syncColors,
}: TagCloudChartProps) => {
- const chartDiv = useRef(null);
- const visController = useRef(null);
+ const [warning, setWarning] = useState(false);
+ const { bucket, metric, scale, palette, showLabel, orientation } = visParams;
+ const bucketFormatter = bucket ? getFormatService().deserialize(bucket.format) : null;
- useEffect(() => {
- if (chartDiv.current) {
- visController.current = new TagCloudVisualization(chartDiv.current, colors, fireEvent);
- }
- return () => {
- visController.current.destroy();
- visController.current = null;
- };
- }, [colors, fireEvent]);
-
- useEffect(() => {
- if (visController.current) {
- visController.current.render(visData, visParams).then(renderComplete);
- }
- }, [visData, visParams, renderComplete]);
+ const tagCloudData = useMemo(() => {
+ const tagColumn = bucket ? visData.columns[bucket.accessor].id : -1;
+ const metricColumn = visData.columns[metric.accessor]?.id;
+
+ const metrics = visData.rows.map((row) => row[metricColumn]);
+ const values = bucket ? visData.rows.map((row) => row[tagColumn]) : [];
+ const maxValue = Math.max(...metrics);
+ const minValue = Math.min(...metrics);
+
+ return visData.rows.map((row) => {
+ const tag = row[tagColumn] === undefined ? 'all' : row[tagColumn];
+ return {
+ text: (bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag) as string,
+ weight:
+ tag === 'all' || visData.rows.length <= 1
+ ? 1
+ : calculateWeight(row[metricColumn], minValue, maxValue, 0, 1) || 0,
+ color: getColor(palettesRegistry, palette.name, tag, values, syncColors) || 'rgba(0,0,0,0)',
+ };
+ });
+ }, [
+ bucket,
+ bucketFormatter,
+ metric.accessor,
+ palette.name,
+ palettesRegistry,
+ syncColors,
+ visData.columns,
+ visData.rows,
+ ]);
+
+ const label = bucket
+ ? `${visData.columns[bucket.accessor].name} - ${visData.columns[metric.accessor].name}`
+ : '';
+
+ const onRenderChange = useCallback(
+ (isRendered) => {
+ if (isRendered) {
+ renderComplete();
+ }
+ },
+ [renderComplete]
+ );
- const updateChartSize = useMemo(
+ const updateChart = useMemo(
() =>
throttle(() => {
- if (visController.current) {
- visController.current.render(visData, visParams).then(renderComplete);
- }
+ setWarning(false);
}, 300),
- [renderComplete, visData, visParams]
+ []
+ );
+
+ const handleWordClick = useCallback(
+ (d) => {
+ if (!bucket) {
+ return;
+ }
+ const termsBucket = visData.columns[bucket.accessor];
+ const clickedValue = d[0][0].text;
+
+ const rowIndex = visData.rows.findIndex((row) => {
+ const formattedValue = bucketFormatter
+ ? bucketFormatter.convert(row[termsBucket.id], 'text')
+ : row[termsBucket.id];
+ return formattedValue === clickedValue;
+ });
+
+ if (rowIndex < 0) {
+ return;
+ }
+
+ fireEvent({
+ name: 'filterBucket',
+ data: {
+ data: [
+ {
+ table: visData,
+ column: bucket.accessor,
+ row: rowIndex,
+ },
+ ],
+ },
+ });
+ },
+ [bucket, bucketFormatter, fireEvent, visData]
);
return (
-
+
{(resizeRef) => (
-
-
+
+
+
+ {
+ setWarning(true);
+ }}
+ />
+
+ {label && showLabel && (
+
+ {label}
+
+ )}
+ {warning && (
+
+
+ }
+ />
+
+ )}
+ {tagCloudData.length > MAX_TAG_COUNT && (
+
+
+ }
+ />
+
+ )}
)}
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx
index d5e005a638680..6682799a8038a 100644
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx
@@ -6,16 +6,22 @@
* Side Public License, v 1.
*/
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import { EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { VisEditorOptionsProps } from 'src/plugins/visualizations/public';
-import { SelectOption, SwitchOption } from '../../../vis_default_editor/public';
+import type { PaletteRegistry } from '../../../charts/public';
+import { VisEditorOptionsProps } from '../../../visualizations/public';
+import { SelectOption, SwitchOption, PalettePicker } from '../../../vis_default_editor/public';
import { ValidatedDualRange } from '../../../kibana_react/public';
-import { TagCloudVisParams } from '../types';
+import { TagCloudVisParams, TagCloudTypeProps } from '../types';
import { collections } from './collections';
-function TagCloudOptions({ stateParams, setValue }: VisEditorOptionsProps
) {
+interface TagCloudOptionsProps
+ extends VisEditorOptionsProps,
+ TagCloudTypeProps {}
+
+function TagCloudOptions({ stateParams, setValue, palettes }: TagCloudOptionsProps) {
+ const [palettesRegistry, setPalettesRegistry] = useState(undefined);
const handleFontSizeChange = ([minFontSize, maxFontSize]: [string | number, string | number]) => {
setValue('minFontSize', Number(minFontSize));
setValue('maxFontSize', Number(maxFontSize));
@@ -24,6 +30,14 @@ function TagCloudOptions({ stateParams, setValue }: VisEditorOptionsProps {
+ const fetchPalettes = async () => {
+ const palettesService = await palettes?.getPalettes();
+ setPalettesRegistry(palettesService);
+ };
+ fetchPalettes();
+ }, [palettes]);
+
return (
+ {palettesRegistry && (
+ {
+ setValue(paramName, value);
+ }}
+ />
+ )}
+
{
- if (!this._visParams.bucket) {
- return;
- }
-
- fireEvent({
- name: 'filterBucket',
- data: {
- data: [
- {
- table: event.meta.data,
- column: 0,
- row: event.meta.rowIndex,
- },
- ],
- },
- });
- });
- this._renderComplete$ = Rx.fromEvent(this._tagCloud, 'renderComplete');
-
- this._feedbackNode = document.createElement('div');
- this._containerNode.appendChild(this._feedbackNode);
- this._feedbackMessage = React.createRef();
- render(
-
-
- ,
- this._feedbackNode
- );
-
- this._labelNode = document.createElement('div');
- this._containerNode.appendChild(this._labelNode);
- this._label = React.createRef();
- render( , this._labelNode);
- }
-
- async render(data, visParams) {
- this._updateParams(visParams);
- this._updateData(data);
- this._resize();
-
- await this._renderComplete$.pipe(take(1)).toPromise();
-
- if (data && data.columns.length !== 2 && this._feedbackMessage.current) {
- this._feedbackMessage.current.setState({
- shouldShowTruncate: false,
- shouldShowIncomplete: false,
- });
- return;
- }
-
- if (data && this._label.current) {
- this._label.current.setState({
- label: `${data.columns[0].name} - ${data.columns[1].name}`,
- shouldShowLabel: visParams.showLabel,
- });
- }
-
- if (this._feedbackMessage.current) {
- this._feedbackMessage.current.setState({
- shouldShowTruncate: this._truncated,
- shouldShowIncomplete: this._tagCloud.getStatus() === TagCloud.STATUS.INCOMPLETE,
- });
- }
- }
-
- destroy() {
- this._tagCloud.destroy();
- unmountComponentAtNode(this._feedbackNode);
- unmountComponentAtNode(this._labelNode);
- }
-
- _updateData(data) {
- if (!data || !data.rows.length) {
- this._tagCloud.setData([]);
- return;
- }
-
- const bucket = this._visParams.bucket;
- const metric = this._visParams.metric;
- const bucketFormatter = bucket ? getFormatService().deserialize(bucket.format) : null;
- const tagColumn = bucket ? data.columns[bucket.accessor].id : -1;
- const metricColumn = data.columns[metric.accessor].id;
- const tags = data.rows.map((row, rowIndex) => {
- const tag = row[tagColumn] === undefined ? 'all' : row[tagColumn];
- const metric = row[metricColumn];
- return {
- displayText: bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag,
- rawText: tag,
- value: metric,
- meta: {
- data: data,
- rowIndex: rowIndex,
- },
- };
- });
-
- if (tags.length > MAX_TAG_COUNT) {
- tags.length = MAX_TAG_COUNT;
- this._truncated = true;
- } else {
- this._truncated = false;
- }
-
- this._tagCloud.setData(tags);
- }
-
- _updateParams(visParams) {
- this._visParams = visParams;
- this._tagCloud.setOptions(visParams);
- }
-
- _resize() {
- this._tagCloud.resize();
- }
-}
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js
deleted file mode 100644
index 26da8b7e72dd1..0000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import 'jest-canvas-mock';
-
-import { TagCloudVisualization } from './tag_cloud_visualization';
-import { setFormatService } from '../services';
-import { dataPluginMock } from '../../../data/public/mocks';
-import { setHTMLElementOffset, setSVGElementGetBBox } from '@kbn/test/jest';
-
-const seedColors = ['#00a69b', '#57c17b', '#6f87d8', '#663db8', '#bc52bc', '#9e3533', '#daa05d'];
-
-describe('TagCloudVisualizationTest', () => {
- let domNode;
- let visParams;
- let SVGElementGetBBoxSpyInstance;
- let HTMLElementOffsetMockInstance;
-
- const dummyTableGroup = {
- columns: [
- {
- id: 'col-0',
- title: 'geo.dest: Descending',
- },
- {
- id: 'col-1',
- title: 'Count',
- },
- ],
- rows: [
- { 'col-0': 'CN', 'col-1': 26 },
- { 'col-0': 'IN', 'col-1': 17 },
- { 'col-0': 'US', 'col-1': 6 },
- { 'col-0': 'DE', 'col-1': 4 },
- { 'col-0': 'BR', 'col-1': 3 },
- ],
- };
-
- const originTransformSVGElement = window.SVGElement.prototype.transform;
-
- beforeAll(() => {
- setFormatService(dataPluginMock.createStartContract().fieldFormats);
- Object.defineProperties(window.SVGElement.prototype, {
- transform: {
- get: () => ({
- baseVal: {
- consolidate: () => {},
- },
- }),
- configurable: true,
- },
- });
- });
-
- afterAll(() => {
- SVGElementGetBBoxSpyInstance.mockRestore();
- HTMLElementOffsetMockInstance.mockRestore();
- window.SVGElement.prototype.transform = originTransformSVGElement;
- });
-
- describe('TagCloudVisualization - basics', () => {
- beforeEach(async () => {
- setupDOM(512, 512);
-
- visParams = {
- bucket: { accessor: 0, format: {} },
- metric: { accessor: 0, format: {} },
- scale: 'linear',
- orientation: 'single',
- };
- });
-
- test('simple draw', async () => {
- const tagcloudVisualization = new TagCloudVisualization(domNode, {
- seedColors,
- });
-
- await tagcloudVisualization.render(dummyTableGroup, visParams);
-
- const svgNode = domNode.querySelector('svg');
- expect(svgNode.outerHTML).toMatchSnapshot();
- });
-
- test('with resize', async () => {
- const tagcloudVisualization = new TagCloudVisualization(domNode, {
- seedColors,
- });
- await tagcloudVisualization.render(dummyTableGroup, visParams);
-
- await tagcloudVisualization.render(dummyTableGroup, visParams);
-
- const svgNode = domNode.querySelector('svg');
- expect(svgNode.outerHTML).toMatchSnapshot();
- });
-
- test('with param change', async function () {
- const tagcloudVisualization = new TagCloudVisualization(domNode, {
- seedColors,
- });
- await tagcloudVisualization.render(dummyTableGroup, visParams);
-
- SVGElementGetBBoxSpyInstance.mockRestore();
- SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(256, 368);
-
- HTMLElementOffsetMockInstance.mockRestore();
- HTMLElementOffsetMockInstance = setHTMLElementOffset(256, 386);
-
- visParams.orientation = 'right angled';
- visParams.minFontSize = 70;
- await tagcloudVisualization.render(dummyTableGroup, visParams);
-
- const svgNode = domNode.querySelector('svg');
- expect(svgNode.outerHTML).toMatchSnapshot();
- });
- });
-
- function setupDOM(width, height) {
- domNode = document.createElement('div');
-
- HTMLElementOffsetMockInstance = setHTMLElementOffset(width, height);
- SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(width, height);
- }
-});
diff --git a/src/plugins/vis_type_tagcloud/public/plugin.ts b/src/plugins/vis_type_tagcloud/public/plugin.ts
index a48e0726e45fe..b2414762f6e47 100644
--- a/src/plugins/vis_type_tagcloud/public/plugin.ts
+++ b/src/plugins/vis_type_tagcloud/public/plugin.ts
@@ -12,7 +12,7 @@ import { VisualizationsSetup } from '../../visualizations/public';
import { ChartsPluginSetup } from '../../charts/public';
import { createTagCloudFn } from './tag_cloud_fn';
-import { tagCloudVisTypeDefinition } from './tag_cloud_type';
+import { getTagCloudVisTypeDefinition } from './tag_cloud_type';
import { DataPublicPluginStart } from '../../data/public';
import { setFormatService } from './services';
import { ConfigSchema } from '../config';
@@ -27,7 +27,7 @@ export interface TagCloudPluginSetupDependencies {
/** @internal */
export interface TagCloudVisDependencies {
- colors: ChartsPluginSetup['legacyColors'];
+ palettes: ChartsPluginSetup['palettes'];
}
/** @internal */
@@ -48,11 +48,15 @@ export class TagCloudPlugin implements Plugin {
{ expressions, visualizations, charts }: TagCloudPluginSetupDependencies
) {
const visualizationDependencies: TagCloudVisDependencies = {
- colors: charts.legacyColors,
+ palettes: charts.palettes,
};
expressions.registerFunction(createTagCloudFn);
expressions.registerRenderer(getTagCloudVisRenderer(visualizationDependencies));
- visualizations.createBaseVisualization(tagCloudVisTypeDefinition);
+ visualizations.createBaseVisualization(
+ getTagCloudVisTypeDefinition({
+ palettes: charts.palettes,
+ })
+ );
}
public start(core: CoreStart, { data }: TagCloudVisPluginStartDependencies) {
diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts
index 0b6a224eee7b5..5dcdffffde01d 100644
--- a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts
@@ -24,6 +24,7 @@ describe('interpreter/functions#tagcloud', () => {
maxFontSize: 72,
showLabel: true,
metric: { accessor: 0, format: { id: 'number' } },
+ bucket: { accessor: 1, format: { id: 'number' } },
};
it('returns an object with the correct structure', () => {
diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts
index d831ba8c760df..17855db9150b5 100644
--- a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts
@@ -9,19 +9,19 @@
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public';
-import { TagCloudVisParams } from './types';
+import { TagCloudVisParams, TagCloudVisConfig } from './types';
const name = 'tagcloud';
-interface Arguments extends TagCloudVisParams {
- metric: any; // these aren't typed yet
- bucket?: any; // these aren't typed yet
+interface Arguments extends TagCloudVisConfig {
+ palette: string;
}
export interface TagCloudVisRenderValue {
visType: typeof name;
visData: Datatable;
- visParams: Arguments;
+ visParams: TagCloudVisParams;
+ syncColors: boolean;
}
export type TagcloudExpressionFunctionDefinition = ExpressionFunctionDefinition<
@@ -70,6 +70,13 @@ export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({
default: true,
help: '',
},
+ palette: {
+ types: ['string'],
+ help: i18n.translate('visTypeTagCloud.function.paletteHelpText', {
+ defaultMessage: 'Defines the chart palette name',
+ }),
+ default: 'default',
+ },
metric: {
types: ['vis_dimension'],
help: i18n.translate('visTypeTagCloud.function.metric.help', {
@@ -92,11 +99,14 @@ export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({
maxFontSize: args.maxFontSize,
showLabel: args.showLabel,
metric: args.metric,
- } as Arguments;
-
- if (args.bucket !== undefined) {
- visParams.bucket = args.bucket;
- }
+ ...(args.bucket && {
+ bucket: args.bucket,
+ }),
+ palette: {
+ type: 'palette',
+ name: args.palette,
+ },
+ } as TagCloudVisParams;
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', input);
@@ -108,6 +118,7 @@ export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({
visData: input,
visType: name,
visParams,
+ syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
},
};
},
diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
index 960122c178caa..b3ab5cd3d7af7 100644
--- a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
@@ -10,10 +10,11 @@ import { i18n } from '@kbn/i18n';
import { AggGroupNames } from '../../data/public';
import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public';
-import { TagCloudOptions } from './components/tag_cloud_options';
+import { getTagCloudOptions } from './components/get_tag_cloud_options';
import { toExpressionAst } from './to_ast';
+import { TagCloudVisDependencies } from './plugin';
-export const tagCloudVisTypeDefinition = {
+export const getTagCloudVisTypeDefinition = ({ palettes }: TagCloudVisDependencies) => ({
name: 'tagcloud',
title: i18n.translate('visTypeTagCloud.vis.tagCloudTitle', { defaultMessage: 'Tag cloud' }),
icon: 'visTagCloud',
@@ -30,11 +31,17 @@ export const tagCloudVisTypeDefinition = {
minFontSize: 18,
maxFontSize: 72,
showLabel: true,
+ palette: {
+ name: 'default',
+ type: 'palette',
+ },
},
},
toExpressionAst,
editorConfig: {
- optionsTemplate: TagCloudOptions,
+ optionsTemplate: getTagCloudOptions({
+ palettes,
+ }),
schemas: [
{
group: AggGroupNames.Metrics,
@@ -69,4 +76,4 @@ export const tagCloudVisTypeDefinition = {
],
},
requiresSearch: true,
-};
+});
diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx b/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx
index 3f05c35ab1dbb..279bfdfffee67 100644
--- a/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx
@@ -8,6 +8,7 @@
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
+import { I18nProvider } from '@kbn/i18n/react';
import { VisualizationContainer } from '../../visualizations/public';
import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers';
@@ -18,7 +19,7 @@ const TagCloudChart = lazy(() => import('./components/tag_cloud_chart'));
export const getTagCloudVisRenderer: (
deps: TagCloudVisDependencies
-) => ExpressionRenderDefinition = ({ colors }) => ({
+) => ExpressionRenderDefinition = ({ palettes }) => ({
name: 'tagloud_vis',
displayName: 'Tag Cloud visualization',
reuseDomNode: true,
@@ -26,16 +27,20 @@ export const getTagCloudVisRenderer: (
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
+ const palettesRegistry = await palettes.getPalettes();
render(
-
-
- ,
+
+
+
+
+ ,
domNode
);
},
diff --git a/src/plugins/vis_type_tagcloud/public/to_ast.test.ts b/src/plugins/vis_type_tagcloud/public/to_ast.test.ts
index 186f621f583d7..4da9c525a4f93 100644
--- a/src/plugins/vis_type_tagcloud/public/to_ast.test.ts
+++ b/src/plugins/vis_type_tagcloud/public/to_ast.test.ts
@@ -66,6 +66,11 @@ describe('tagcloud vis toExpressionAst function', () => {
minFontSize: 5,
maxFontSize: 15,
showLabel: true,
+ palette: {
+ type: 'palette',
+ name: 'default',
+ },
+ metric: { accessor: 0, format: { id: 'number' } },
};
const actual = toExpressionAst(vis, {} as any);
expect(actual).toMatchSnapshot();
diff --git a/src/plugins/vis_type_tagcloud/public/to_ast.ts b/src/plugins/vis_type_tagcloud/public/to_ast.ts
index 38f2ef9271b3d..8a2fb4e843973 100644
--- a/src/plugins/vis_type_tagcloud/public/to_ast.ts
+++ b/src/plugins/vis_type_tagcloud/public/to_ast.ts
@@ -39,7 +39,7 @@ export const toExpressionAst: VisToExpressionAst = (vis, para
});
const schemas = getVisSchemas(vis, params);
- const { scale, orientation, minFontSize, maxFontSize, showLabel } = vis.params;
+ const { scale, orientation, minFontSize, maxFontSize, showLabel, palette } = vis.params;
const tagcloud = buildExpressionFunction('tagcloud', {
scale,
@@ -48,6 +48,7 @@ export const toExpressionAst: VisToExpressionAst = (vis, para
maxFontSize,
showLabel,
metric: prepareDimension(schemas.metric[0]),
+ palette: palette?.name,
});
if (schemas.segment) {
diff --git a/src/plugins/vis_type_tagcloud/public/types.ts b/src/plugins/vis_type_tagcloud/public/types.ts
index d1c63d8c634bb..7105476670693 100644
--- a/src/plugins/vis_type_tagcloud/public/types.ts
+++ b/src/plugins/vis_type_tagcloud/public/types.ts
@@ -5,11 +5,37 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+import type { ChartsPluginSetup, PaletteOutput } from '../../charts/public';
+import type { SerializedFieldFormat } from '../../expressions/public';
+import { ExpressionValueVisDimension } from '../../visualizations/public';
-export interface TagCloudVisParams {
+interface Dimension {
+ accessor: number;
+ format: {
+ id?: string;
+ params?: SerializedFieldFormat;
+ };
+}
+
+interface TagCloudCommonParams {
scale: 'linear' | 'log' | 'square root';
orientation: 'single' | 'right angled' | 'multiple';
minFontSize: number;
maxFontSize: number;
showLabel: boolean;
}
+
+export interface TagCloudVisConfig extends TagCloudCommonParams {
+ metric: ExpressionValueVisDimension;
+ bucket?: ExpressionValueVisDimension;
+}
+
+export interface TagCloudVisParams extends TagCloudCommonParams {
+ palette: PaletteOutput;
+ metric: Dimension;
+ bucket?: Dimension;
+}
+
+export interface TagCloudTypeProps {
+ palettes: ChartsPluginSetup['palettes'];
+}
diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx
index 3ccdfb7e47d70..872132416352f 100644
--- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx
+++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx
@@ -104,6 +104,9 @@ export class VisualizeEmbeddableFactory
}
return visType.stage !== 'experimental';
},
+ getSavedObjectSubType: (savedObject) => {
+ return JSON.parse(savedObject.attributes.visState).type;
+ },
};
constructor(private readonly deps: VisualizeEmbeddableFactoryDeps) {}
diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts
index 2be9358e28d1a..a8b00b15a1ede 100644
--- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts
+++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts
@@ -7,6 +7,7 @@
*/
import { SavedObject } from '../../../../core/types/saved_objects';
+import { BaseVisType } from './base_vis_type';
export type VisualizationStage = 'experimental' | 'beta' | 'production';
@@ -23,6 +24,7 @@ export interface VisualizationListItem {
getSupportedTriggers?: () => string[];
typeTitle: string;
image?: string;
+ type?: BaseVisType | string;
}
export interface VisualizationsAppExtension {
diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx
index 317f9d1bb363d..2620ae01aa15a 100644
--- a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx
+++ b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx
@@ -153,7 +153,7 @@ class NewVisModal extends React.Component {
@@ -52,6 +53,13 @@ const byValueMigrateVislibPie = (state: SerializableState) => {
};
};
+const byValueMigrateTagcloud = (state: SerializableState) => {
+ return {
+ ...state,
+ savedVis: commonMigrateTagCloud(state.savedVis),
+ };
+};
+
export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => {
return {
id: 'visualization',
@@ -63,7 +71,8 @@ export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => {
byValueHideTSVBLastValueIndicator,
byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel
)(state),
- '7.14.0': (state) => flow(byValueAddEmptyValueColorRule, byValueMigrateVislibPie)(state),
+ '7.14.0': (state) =>
+ flow(byValueAddEmptyValueColorRule, byValueMigrateVislibPie, byValueMigrateTagcloud)(state),
},
};
};
diff --git a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
index f5afeee0ff35e..17b1470a40062 100644
--- a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
@@ -114,3 +114,25 @@ export const commonMigrateVislibPie = (visState: any) => {
return visState;
};
+
+export const commonMigrateTagCloud = (visState: any) => {
+ if (visState && visState.type === 'tagcloud') {
+ const { params } = visState;
+ const hasPalette = params?.palette;
+
+ return {
+ ...visState,
+ params: {
+ ...visState.params,
+ ...(!hasPalette && {
+ palette: {
+ type: 'palette',
+ name: 'kibana_palette',
+ },
+ }),
+ },
+ };
+ }
+
+ return visState;
+};
diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
index 7ee43f36c864e..7debc9412925e 100644
--- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
@@ -2162,4 +2162,45 @@ describe('migration visualization', () => {
expect(distinctColors).toBe(true);
});
});
+
+ describe('7.14.0 update tagcloud defaults', () => {
+ const migrate = (doc: any) =>
+ visualizationSavedObjectTypeMigrations['7.14.0'](
+ doc as Parameters[0],
+ savedObjectMigrationContext
+ );
+ const getTestDoc = (hasPalette = false) => ({
+ attributes: {
+ title: 'My Vis',
+ description: 'This is my super cool vis.',
+ visState: JSON.stringify({
+ type: 'tagcloud',
+ title: '[Flights] Delay Type',
+ params: {
+ type: 'tagcloud',
+ ...(hasPalette && {
+ palette: {
+ type: 'palette',
+ name: 'default',
+ },
+ }),
+ },
+ }),
+ },
+ });
+
+ it('should decorate existing docs with the kibana legacy palette if the palette is not defined - pie', () => {
+ const migratedTestDoc = migrate(getTestDoc());
+ const { palette } = JSON.parse(migratedTestDoc.attributes.visState).params;
+
+ expect(palette.name).toEqual('kibana_palette');
+ });
+
+ it('should not overwrite the palette with the legacy one if the palette already exists in the saved object', () => {
+ const migratedTestDoc = migrate(getTestDoc(true));
+ const { palette } = JSON.parse(migratedTestDoc.attributes.visState).params;
+
+ expect(palette.name).toEqual('default');
+ });
+ });
});
diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
index f386d9eb12091..7fb54b0425935 100644
--- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
@@ -17,6 +17,7 @@ import {
commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel,
commonMigrateVislibPie,
commonAddEmptyValueColorRule,
+ commonMigrateTagCloud,
} from './visualization_common_migrations';
const migrateIndexPattern: SavedObjectMigrationFn = (doc) => {
@@ -1014,6 +1015,29 @@ const migrateVislibPie: SavedObjectMigrationFn = (doc) => {
return doc;
};
+// [Tagcloud] Migrate to the new palette service
+const migrateTagCloud: SavedObjectMigrationFn = (doc) => {
+ const visStateJSON = get(doc, 'attributes.visState');
+ let visState;
+
+ if (visStateJSON) {
+ try {
+ visState = JSON.parse(visStateJSON);
+ } catch (e) {
+ // Let it go, the data is invalid and we'll leave it as is
+ }
+ const newVisState = commonMigrateTagCloud(visState);
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ visState: JSON.stringify(newVisState),
+ },
+ };
+ }
+ return doc;
+};
+
export const visualizationSavedObjectTypeMigrations = {
/**
* We need to have this migration twice, once with a version prior to 7.0.0 once with a version
@@ -1060,5 +1084,5 @@ export const visualizationSavedObjectTypeMigrations = {
hideTSVBLastValueIndicator,
removeDefaultIndexPatternAndTimeFieldFromTSVBModel
),
- '7.14.0': flow(addEmptyValueColorRule, migrateVislibPie),
+ '7.14.0': flow(addEmptyValueColorRule, migrateVislibPie, migrateTagCloud),
};
diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json
index 144d33debe3c9..6570e79bb4ce6 100644
--- a/src/plugins/visualize/kibana.json
+++ b/src/plugins/visualize/kibana.json
@@ -16,7 +16,8 @@
"optionalPlugins": [
"home",
"share",
- "savedObjectsTaggingOss"
+ "savedObjectsTaggingOss",
+ "usageCollection"
],
"requiredBundles": [
"kibanaUtils",
diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts
index da18b3b97a522..f850aedc33366 100644
--- a/src/plugins/visualize/public/application/types.ts
+++ b/src/plugins/visualize/public/application/types.ts
@@ -6,9 +6,19 @@
* Side Public License, v 1.
*/
-import { History } from 'history';
-import { Query, Filter, DataPublicPluginStart, TimeRange } from 'src/plugins/data/public';
-import {
+import type { EventEmitter } from 'events';
+import type { History } from 'history';
+
+import type {
+ CoreStart,
+ PluginInitializerContext,
+ ChromeStart,
+ ToastsStart,
+ ScopedHistory,
+ AppMountParameters,
+} from 'kibana/public';
+
+import type {
SavedVisState,
VisualizationsStart,
Vis,
@@ -17,28 +27,23 @@ import {
PersistedState,
VisParams,
} from 'src/plugins/visualizations/public';
-import {
- CoreStart,
- PluginInitializerContext,
- ChromeStart,
- ToastsStart,
- ScopedHistory,
- AppMountParameters,
-} from 'kibana/public';
-import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public';
-import {
+
+import type {
Storage,
IKbnUrlStateStorage,
ReduxLikeStateContainer,
} from 'src/plugins/kibana_utils/public';
-import { SharePluginStart } from 'src/plugins/share/public';
-import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public';
-import { EmbeddableStart, EmbeddableStateTransfer } from 'src/plugins/embeddable/public';
-import { UrlForwardingStart } from 'src/plugins/url_forwarding/public';
-import { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public';
-import { EventEmitter } from 'events';
-import { DashboardStart } from '../../../dashboard/public';
+
+import type { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public';
+import type { Query, Filter, DataPublicPluginStart, TimeRange } from 'src/plugins/data/public';
+import type { SharePluginStart } from 'src/plugins/share/public';
+import type { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public';
+import type { EmbeddableStart, EmbeddableStateTransfer } from 'src/plugins/embeddable/public';
+import type { UrlForwardingStart } from 'src/plugins/url_forwarding/public';
+import type { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public';
+import type { DashboardStart } from '../../../dashboard/public';
import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public';
+import type { UsageCollectionStart } from '../../../usage_collection/public';
export type PureVisState = SavedVisState;
@@ -97,6 +102,7 @@ export interface VisualizeServices extends CoreStart {
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
savedObjectsTagging?: SavedObjectsTaggingApi;
presentationUtil: PresentationUtilPluginStart;
+ usageCollection?: UsageCollectionStart;
}
export interface SavedVisInstance {
diff --git a/src/plugins/visualize/public/application/utils/get_table_columns.tsx b/src/plugins/visualize/public/application/utils/get_table_columns.tsx
index ac8fee452aa6c..8c8ecaf9a448a 100644
--- a/src/plugins/visualize/public/application/utils/get_table_columns.tsx
+++ b/src/plugins/visualize/public/application/utils/get_table_columns.tsx
@@ -7,6 +7,7 @@
*/
import React from 'react';
+import { METRIC_TYPE } from '@kbn/analytics';
import { EuiBetaBadge, EuiButton, EuiEmptyPrompt, EuiIcon, EuiLink, EuiBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -16,6 +17,16 @@ import { VisualizationListItem } from 'src/plugins/visualizations/public';
import type { SavedObjectsTaggingApi } from 'src/plugins/saved_objects_tagging_oss/public';
import { RedirectAppLinks } from '../../../../kibana_react/public';
import { getVisualizeListItemLink } from './get_visualize_list_item_link';
+import { getUsageCollector } from '../../services';
+import { APP_NAME } from '../visualize_constants';
+
+const doTelemetryForAddEvent = (visType?: string) => {
+ const usageCollection = getUsageCollector();
+
+ if (usageCollection && visType) {
+ usageCollection.reportUiCounter(APP_NAME, METRIC_TYPE.CLICK, `${visType}:add`);
+ }
+};
const getBadge = (item: VisualizationListItem) => {
if (item.stage === 'beta') {
@@ -82,12 +93,16 @@ export const getTableColumns = (
defaultMessage: 'Title',
}),
sortable: true,
- render: (field: string, { editApp, editUrl, title, error }: VisualizationListItem) =>
+ render: (field: string, { editApp, editUrl, title, error, type }: VisualizationListItem) =>
// In case an error occurs i.e. the vis has wrong type, we render the vis but without the link
!error ? (
+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
{
+ doTelemetryForAddEvent(typeof type === 'string' ? type : type?.name);
+ }}
data-test-subj={`visListingTitleLink-${title.split(' ').join('-')}`}
>
{field}
diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
index b7c7d63cef98f..da01f9d44879b 100644
--- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
+++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
+import { METRIC_TYPE } from '@kbn/analytics';
import { Capabilities } from 'src/core/public';
import { TopNavMenuData } from 'src/plugins/navigation/public';
@@ -29,7 +30,7 @@ import {
VisualizeAppStateContainer,
VisualizeEditorVisInstance,
} from '../types';
-import { VisualizeConstants } from '../visualize_constants';
+import { APP_NAME, VisualizeConstants } from '../visualize_constants';
import { getEditBreadcrumbs } from './breadcrumbs';
import { EmbeddableStateTransfer } from '../../../../embeddable/public';
@@ -92,10 +93,22 @@ export const getTopNavConfig = (
dashboard,
savedObjectsTagging,
presentationUtil,
+ usageCollection,
}: VisualizeServices
) => {
const { vis, embeddableHandler } = visInstance;
const savedVis = visInstance.savedVis;
+
+ const doTelemetryForSaveEvent = (visType: string) => {
+ if (usageCollection) {
+ usageCollection.reportUiCounter(
+ originatingApp ?? APP_NAME,
+ METRIC_TYPE.CLICK,
+ `${visType}:save`
+ );
+ }
+ };
+
/**
* Called when the user clicks "Save" button.
*/
@@ -394,6 +407,8 @@ export const getTopNavConfig = (
return { id: true };
}
+ doTelemetryForSaveEvent(vis.type.name);
+
// We're adding the viz to a library so we need to save it and then
// add to a dashboard if necessary
const response = await doSave(saveOptions);
@@ -503,6 +518,8 @@ export const getTopNavConfig = (
}
},
run: async () => {
+ doTelemetryForSaveEvent(vis.type.name);
+
if (!savedVis?.id) {
return createVisReference();
}
diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts
index 4b369e8be86ee..b5ddbdf6d10a3 100644
--- a/src/plugins/visualize/public/plugin.ts
+++ b/src/plugins/visualize/public/plugin.ts
@@ -6,10 +6,11 @@
* Side Public License, v 1.
*/
-import { BehaviorSubject } from 'rxjs';
import { i18n } from '@kbn/i18n';
-import { filter, map } from 'rxjs/operators';
import { createHashHistory } from 'history';
+import { BehaviorSubject } from 'rxjs';
+import { filter, map } from 'rxjs/operators';
+
import {
AppMountParameters,
AppUpdater,
@@ -18,29 +19,33 @@ import {
Plugin,
PluginInitializerContext,
ScopedHistory,
-} from 'kibana/public';
+ DEFAULT_APP_CATEGORIES,
+} from '../../../core/public';
-import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public';
import {
Storage,
createKbnUrlTracker,
createKbnUrlStateStorage,
withNotifyOnErrors,
} from '../../kibana_utils/public';
-import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public';
-import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public';
-import { SharePluginStart, SharePluginSetup } from '../../share/public';
-import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public';
-import { VisualizationsStart } from '../../visualizations/public';
+
import { VisualizeConstants } from './application/visualize_constants';
+import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public';
import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public';
-import { VisualizeServices } from './application/types';
-import { DEFAULT_APP_CATEGORIES } from '../../../core/public';
-import { SavedObjectsStart } from '../../saved_objects/public';
-import { EmbeddableStart } from '../../embeddable/public';
-import { DashboardStart } from '../../dashboard/public';
+
+import type { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public';
+import type { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public';
+import type { SharePluginStart, SharePluginSetup } from '../../share/public';
+import type { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public';
+import type { VisualizationsStart } from '../../visualizations/public';
+import type { VisualizeServices } from './application/types';
+import type { SavedObjectsStart } from '../../saved_objects/public';
+import type { EmbeddableStart } from '../../embeddable/public';
+import type { DashboardStart } from '../../dashboard/public';
import type { SavedObjectTaggingOssPluginStart } from '../../saved_objects_tagging_oss/public';
-import { setVisEditorsRegistry, setUISettings } from './services';
+import type { UsageCollectionStart } from '../../usage_collection/public';
+
+import { setVisEditorsRegistry, setUISettings, setUsageCollector } from './services';
import { createVisEditorsRegistry, VisEditorsRegistry } from './vis_editors_registry';
export interface VisualizePluginStartDependencies {
@@ -54,6 +59,7 @@ export interface VisualizePluginStartDependencies {
dashboard: DashboardStart;
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
presentationUtil: PresentationUtilPluginStart;
+ usageCollection?: UsageCollectionStart;
}
export interface VisualizePluginSetupDependencies {
@@ -202,6 +208,7 @@ export class VisualizePlugin
setHeaderActionMenu: params.setHeaderActionMenu,
savedObjectsTagging: pluginsStart.savedObjectsTaggingOss?.getTaggingApi(),
presentationUtil: pluginsStart.presentationUtil,
+ usageCollection: pluginsStart.usageCollection,
};
params.element.classList.add('visAppWrapper');
@@ -238,8 +245,12 @@ export class VisualizePlugin
} as VisualizePluginSetup;
}
- public start(core: CoreStart, plugins: VisualizePluginStartDependencies) {
+ public start(core: CoreStart, { usageCollection }: VisualizePluginStartDependencies) {
setVisEditorsRegistry(this.visEditorsRegistry);
+
+ if (usageCollection) {
+ setUsageCollector(usageCollection);
+ }
}
stop() {
diff --git a/src/plugins/visualize/public/services.ts b/src/plugins/visualize/public/services.ts
index 192aac3547eb2..97ff7923379b7 100644
--- a/src/plugins/visualize/public/services.ts
+++ b/src/plugins/visualize/public/services.ts
@@ -6,12 +6,18 @@
* Side Public License, v 1.
*/
-import { IUiSettingsClient } from '../../../core/public';
import { createGetterSetter } from '../../../plugins/kibana_utils/public';
-import { VisEditorsRegistry } from './vis_editors_registry';
+
+import type { IUiSettingsClient } from '../../../core/public';
+import type { VisEditorsRegistry } from './vis_editors_registry';
+import type { UsageCollectionStart } from '../../usage_collection/public';
export const [getUISettings, setUISettings] = createGetterSetter('UISettings');
+export const [getUsageCollector, setUsageCollector] = createGetterSetter(
+ 'UsageCollection'
+);
+
export const [
getVisEditorsRegistry,
setVisEditorsRegistry,
diff --git a/test/accessibility/apps/management.ts b/test/accessibility/apps/management.ts
index 692b140ade7ee..e71f6bb3ebfee 100644
--- a/test/accessibility/apps/management.ts
+++ b/test/accessibility/apps/management.ts
@@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const a11y = getService('a11y');
+ const testSubjects = getService('testSubjects');
describe('Management', () => {
before(async () => {
@@ -43,6 +44,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await a11y.testAppSnapshot();
});
+ it('Index pattern field editor - initial view', async () => {
+ await PageObjects.settings.clickAddField();
+ await a11y.testAppSnapshot();
+ });
+
+ it('Index pattern field editor - all options shown', async () => {
+ await PageObjects.settings.setFieldName('test');
+ await PageObjects.settings.setFieldType('Keyword');
+ await PageObjects.settings.setFieldScript("emit('hello world')");
+ await PageObjects.settings.toggleRow('formatRow');
+ await PageObjects.settings.setFieldFormat('string');
+ await PageObjects.settings.toggleRow('customLabelRow');
+ await PageObjects.settings.setCustomLabel('custom label');
+ await testSubjects.click('toggleAdvancedSetting');
+
+ await a11y.testAppSnapshot();
+
+ await testSubjects.click('euiFlyoutCloseButton');
+ await PageObjects.settings.closeIndexPatternFieldEditor();
+ });
+
it('Open create index pattern wizard', async () => {
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickAddNewIndexPatternButton();
diff --git a/test/functional/apps/bundles/index.js b/test/functional/apps/bundles/index.js
index d13e74dd4eed9..577035a8c343c 100644
--- a/test/functional/apps/bundles/index.js
+++ b/test/functional/apps/bundles/index.js
@@ -18,6 +18,15 @@ export default function ({ getService }) {
let buildNum;
before(async () => {
+ // Wait for status to become green
+ let status;
+ const start = Date.now();
+ do {
+ const resp = await supertest.get('/api/status');
+ status = resp.status;
+ // Stop polling once status stabilizes OR once 40s has passed
+ } while (status !== 200 && Date.now() - start < 40_000);
+
const resp = await supertest.get('/api/status').expect(200);
buildNum = resp.body.version.build_number;
});
diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts
index b5a40030b2601..5d7c16c3c6408 100644
--- a/test/functional/apps/discover/_doc_table.ts
+++ b/test/functional/apps/discover/_doc_table.ts
@@ -68,7 +68,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
});
- it(`should load up to ${rowsHardLimit} rows when scrolling at the end of the table with `, async function () {
+ it('should load more rows when scrolling down the document table', async function () {
const initialRows = await testSubjects.findAll('docTableRow');
await testSubjects.scrollIntoView('discoverBackToTop');
// now count the rows
@@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
});
- it(`should load up to ${rowsHardLimit} rows when scrolling at the end of the table with `, async function () {
+ it('should load more rows when scrolling down the document table', async function () {
const initialRows = await testSubjects.findAll('docTableRow');
await testSubjects.scrollIntoView('discoverBackToTop');
// now count the rows
diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts
index 648fa3efe337c..46fe5c34f4cf3 100644
--- a/test/functional/apps/discover/_runtime_fields_editor.ts
+++ b/test/functional/apps/discover/_runtime_fields_editor.ts
@@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await fieldEditor.save();
};
- describe.skip('discover integration with runtime fields editor', function describeIndexTests() {
+ describe('discover integration with runtime fields editor', function describeIndexTests() {
before(async function () {
await esArchiver.load('test/functional/fixtures/es_archiver/discover');
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
@@ -104,7 +104,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
- // flaky https://github.com/elastic/kibana/issues/100966
it('doc view includes runtime fields', async function () {
// navigate to doc view
const table = await PageObjects.discover.getDocTable();
@@ -121,10 +120,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await rowActions[idxToClick].click();
});
- const hasDocHit = await testSubjects.exists('doc-hit');
- expect(hasDocHit).to.be(true);
- const runtimeFieldsRow = await testSubjects.exists('tableDocViewRow-discover runtimefield');
- expect(runtimeFieldsRow).to.be(true);
+ await retry.waitFor('doc viewer is displayed with runtime field', async () => {
+ const hasDocHit = await testSubjects.exists('doc-hit');
+ if (!hasDocHit) {
+ // Maybe loading has not completed
+ throw new Error('test subject doc-hit is not yet displayed');
+ }
+ const runtimeFieldsRow = await testSubjects.exists('tableDocViewRow-discover runtimefield');
+
+ return hasDocHit && runtimeFieldsRow;
+ });
});
});
}
diff --git a/test/functional/apps/visualize/_tag_cloud.ts b/test/functional/apps/visualize/_tag_cloud.ts
index a6ac324d9dc61..d18c85f3b58be 100644
--- a/test/functional/apps/visualize/_tag_cloud.ts
+++ b/test/functional/apps/visualize/_tag_cloud.ts
@@ -63,8 +63,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(data).to.eql([
'32,212,254,720',
'21,474,836,480',
- '20,401,094,656',
'19,327,352,832',
+ '20,401,094,656',
'18,253,611,008',
]);
});
@@ -91,8 +91,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(data).to.eql([
'32,212,254,720',
'21,474,836,480',
- '20,401,094,656',
'19,327,352,832',
+ '20,401,094,656',
'18,253,611,008',
]);
});
@@ -106,8 +106,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(data).to.eql([
'32,212,254,720',
'21,474,836,480',
- '20,401,094,656',
'19,327,352,832',
+ '20,401,094,656',
'18,253,611,008',
]);
});
@@ -122,7 +122,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should show the tags and relative size', function () {
return PageObjects.tagCloud.getTextSizes().then(function (results) {
log.debug('results here ' + results);
- expect(results).to.eql(['72px', '63px', '25px', '32px', '18px']);
+ expect(results).to.eql(['72px', '63px', '32px', '25px', '18px']);
});
});
@@ -177,7 +177,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should format tags with field formatter', async function () {
const data = await PageObjects.tagCloud.getTextTag();
log.debug(data);
- expect(data).to.eql(['30GB', '20GB', '19GB', '18GB', '17GB']);
+ expect(data).to.eql(['30GB', '20GB', '18GB', '19GB', '17GB']);
});
it('should apply filter with unformatted value', async function () {
diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts
index 7d7da79b4a397..88951bb04c956 100644
--- a/test/functional/page_objects/settings_page.ts
+++ b/test/functional/page_objects/settings_page.ts
@@ -563,11 +563,8 @@ export class SettingsPageObject extends FtrService {
async setFieldScript(script: string) {
this.log.debug('set script = ' + script);
- const formatRow = await this.testSubjects.find('valueRow');
- const formatRowToggle = (await formatRow.findAllByCssSelector('[data-test-subj="toggle"]'))[0];
-
- await formatRowToggle.click();
- const getMonacoTextArea = async () => (await formatRow.findAllByCssSelector('textarea'))[0];
+ const valueRow = await this.toggleRow('valueRow');
+ const getMonacoTextArea = async () => (await valueRow.findAllByCssSelector('textarea'))[0];
this.retry.waitFor('monaco editor is ready', async () => !!(await getMonacoTextArea()));
const monacoTextArea = await getMonacoTextArea();
await monacoTextArea.focus();
@@ -576,8 +573,8 @@ export class SettingsPageObject extends FtrService {
async changeFieldScript(script: string) {
this.log.debug('set script = ' + script);
- const formatRow = await this.testSubjects.find('valueRow');
- const getMonacoTextArea = async () => (await formatRow.findAllByCssSelector('textarea'))[0];
+ const valueRow = await this.testSubjects.find('valueRow');
+ const getMonacoTextArea = async () => (await valueRow.findAllByCssSelector('textarea'))[0];
this.retry.waitFor('monaco editor is ready', async () => !!(await getMonacoTextArea()));
const monacoTextArea = await getMonacoTextArea();
await monacoTextArea.focus();
@@ -622,6 +619,24 @@ export class SettingsPageObject extends FtrService {
);
}
+ async toggleRow(rowTestSubj: string) {
+ this.log.debug('toggling tow = ' + rowTestSubj);
+ const row = await this.testSubjects.find(rowTestSubj);
+ const rowToggle = (await row.findAllByCssSelector('[data-test-subj="toggle"]'))[0];
+ await rowToggle.click();
+ return row;
+ }
+
+ async setCustomLabel(label: string) {
+ this.log.debug('set custom label = ' + label);
+ await (
+ await this.testSubjects.findDescendant(
+ 'input',
+ await this.testSubjects.find('customLabelRow')
+ )
+ ).type(label);
+ }
+
async setScriptedFieldUrlType(type: string) {
this.log.debug('set scripted field Url type = ' + type);
await this.find.clickByCssSelector(
diff --git a/test/functional/page_objects/tag_cloud_page.ts b/test/functional/page_objects/tag_cloud_page.ts
index 61e844c813df8..ce51959390a42 100644
--- a/test/functional/page_objects/tag_cloud_page.ts
+++ b/test/functional/page_objects/tag_cloud_page.ts
@@ -11,15 +11,24 @@ import { WebElementWrapper } from '../services/lib/web_element_wrapper';
export class TagCloudPageObject extends FtrService {
private readonly find = this.ctx.getService('find');
- private readonly testSubjects = this.ctx.getService('testSubjects');
private readonly header = this.ctx.getPageObject('header');
private readonly visChart = this.ctx.getPageObject('visChart');
public async selectTagCloudTag(tagDisplayText: string) {
- await this.testSubjects.click(tagDisplayText);
+ const elements = await this.find.allByCssSelector('text');
+ const targetElement = elements.find(
+ async (element) => (await element.getVisibleText()) === tagDisplayText
+ );
+ await targetElement?.click();
await this.header.waitUntilLoadingHasFinished();
}
+ public async getTextTagByElement(webElement: WebElementWrapper) {
+ await this.visChart.waitForVisualization();
+ const elements = await webElement.findAllByCssSelector('text');
+ return await Promise.all(elements.map(async (element) => await element.getVisibleText()));
+ }
+
public async getTextTag() {
await this.visChart.waitForVisualization();
const elements = await this.find.allByCssSelector('text');
diff --git a/test/functional/services/dashboard/expectations.ts b/test/functional/services/dashboard/expectations.ts
index c22eddb032cf9..0a689c0091edc 100644
--- a/test/functional/services/dashboard/expectations.ts
+++ b/test/functional/services/dashboard/expectations.ts
@@ -16,8 +16,10 @@ export class DashboardExpectService extends FtrService {
private readonly testSubjects = this.ctx.getService('testSubjects');
private readonly find = this.ctx.getService('find');
private readonly filterBar = this.ctx.getService('filterBar');
+
private readonly dashboard = this.ctx.getPageObject('dashboard');
private readonly visChart = this.ctx.getPageObject('visChart');
+ private readonly tagCloud = this.ctx.getPageObject('tagCloud');
private readonly findTimeout = 2500;
async panelCount(expectedCount: number) {
@@ -166,8 +168,9 @@ export class DashboardExpectService extends FtrService {
const tagCloudVisualizations = await this.testSubjects.findAll('tagCloudVisualization');
const matches = await Promise.all(
tagCloudVisualizations.map(async (tagCloud) => {
+ const tagCloudData = await this.tagCloud.getTextTagByElement(tagCloud);
for (let i = 0; i < values.length; i++) {
- const valueExists = await this.testSubjects.descendantExists(values[i], tagCloud);
+ const valueExists = tagCloudData.includes(values[i]);
if (!valueExists) {
return false;
}
diff --git a/test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js b/test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js
index 88a6ad0e3ab15..514d1bb1d9d7b 100644
--- a/test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js
+++ b/test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js
@@ -27,7 +27,7 @@
* SOFTWARE.
*/
-export function scrollIntoViewIfNecessary(target, fixedHeaderHeight) {
+export function scrollIntoViewIfNecessary(target, fixedHeaderHeight, fixedFooterHeight) {
var rootScroller = document.scrollingElement || document.documentElement;
if (!rootScroller) {
throw new Error('Unable to find document.scrollingElement or document.documentElement');
@@ -63,4 +63,11 @@ export function scrollIntoViewIfNecessary(target, fixedHeaderHeight) {
if (additionalScrollNecessary > 0) {
rootScroller.scrollTop = rootScroller.scrollTop - additionalScrollNecessary;
}
+
+ if (fixedFooterHeight) {
+ var bottomOfVisibility = viewportHeight - fixedFooterHeight;
+ if (bottomOfVisibility < boundingRect.bottom) {
+ rootScroller.scrollTop = rootScroller.scrollTop + fixedFooterHeight;
+ }
+ }
}
diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts
index 148c21ffac191..4b164402bfb70 100644
--- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts
+++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts
@@ -692,11 +692,22 @@ export class WebElementWrapper {
* @nonstandard
* @return {Promise}
*/
- public async scrollIntoViewIfNecessary(topOffset?: number): Promise {
+ public async scrollIntoViewIfNecessary(
+ topOffsetOrOptions?: number | { topOffset?: number; bottomOffset?: number }
+ ): Promise {
+ let topOffset: undefined | number;
+ let bottomOffset: undefined | number;
+ if (typeof topOffsetOrOptions === 'number') {
+ topOffset = topOffsetOrOptions;
+ } else {
+ topOffset = topOffsetOrOptions?.topOffset;
+ bottomOffset = topOffsetOrOptions?.bottomOffset;
+ }
await this.driver.executeScript(
scrollIntoViewIfNecessary,
this._webElement,
- topOffset || this.fixedHeaderHeight
+ topOffset || this.fixedHeaderHeight,
+ bottomOffset
);
}
diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json
index 14c8428c6d432..e0b62688d0662 100644
--- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json
+++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json
index 073fca760b9a2..d85444f5d3b6b 100644
--- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json
+++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json
index 93f8d8a27d233..2c81c9447b826 100644
--- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json
+++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json
index 0c50947beca97..687b669b18e61 100644
--- a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json
+++ b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json
index e8c47efdbe622..b49953f9a023b 100644
--- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json
+++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json
index 38683082975f8..fc7e289dfbd3a 100644
--- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json
+++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"default","type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json
index 14c8428c6d432..e0b62688d0662 100644
--- a/test/interpreter_functional/snapshots/session/partial_test_1.json
+++ b/test/interpreter_functional/snapshots/session/partial_test_1.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json
index 073fca760b9a2..d85444f5d3b6b 100644
--- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json
+++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json
index 93f8d8a27d233..2c81c9447b826 100644
--- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json
+++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json
index 0c50947beca97..687b669b18e61 100644
--- a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json
+++ b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json
index e8c47efdbe622..b49953f9a023b 100644
--- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json
+++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json
index 38683082975f8..fc7e289dfbd3a 100644
--- a/test/interpreter_functional/snapshots/session/tagcloud_options.json
+++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"default","type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/server_integration/http/platform/cache.ts b/test/server_integration/http/platform/cache.ts
index 2c1aa90e963e2..a33f916cf1c4b 100644
--- a/test/server_integration/http/platform/cache.ts
+++ b/test/server_integration/http/platform/cache.ts
@@ -12,6 +12,17 @@ export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('kibana server cache-control', () => {
+ before(async () => {
+ // Wait for status to become green
+ let status;
+ const start = Date.now();
+ do {
+ const resp = await supertest.get('/api/status');
+ status = resp.status;
+ // Stop polling once status stabilizes OR once 40s has passed
+ } while (status !== 200 && Date.now() - start < 40_000);
+ });
+
it('properly marks responses as private, with directives to disable caching', async () => {
await supertest
.get('/api/status')
diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx
index 33bb1f06d045c..6a39951ad4958 100644
--- a/x-pack/examples/embedded_lens_example/public/app.tsx
+++ b/x-pack/examples/embedded_lens_example/public/app.tsx
@@ -24,6 +24,7 @@ import {
TypedLensByValueInput,
PersistedIndexPatternLayer,
XYState,
+ LensEmbeddableInput,
} from '../../../plugins/lens/public';
import { StartDependencies } from './plugin';
@@ -112,12 +113,15 @@ export const App = (props: {
}) => {
const [color, setColor] = useState('green');
const [isLoading, setIsLoading] = useState(false);
+ const [isSaveModalVisible, setIsSaveModalVisible] = useState(false);
const LensComponent = props.plugins.lens.EmbeddableComponent;
+ const LensSaveModalComponent = props.plugins.lens.SaveModalComponent;
const [time, setTime] = useState({
from: 'now-5d',
to: 'now',
});
+
return (
@@ -172,7 +176,18 @@ export const App = (props: {
setColor(newColor);
}}
>
- Edit
+ Edit in Lens
+
+
+
+ {
+ setIsSaveModalVisible(true);
+ }}
+ >
+ Save Visualization
@@ -197,6 +212,19 @@ export const App = (props: {
// call back event for on table row click event
}}
/>
+ {isSaveModalVisible && (
+ {}}
+ onClose={() => setIsSaveModalVisible(false)}
+ />
+ )}
>
) : (
This demo only works if your default index pattern is set and time based
diff --git a/x-pack/examples/embedded_lens_example/public/mount.tsx b/x-pack/examples/embedded_lens_example/public/mount.tsx
index 5cf7c25fbf160..ff1e6ef8818f0 100644
--- a/x-pack/examples/embedded_lens_example/public/mount.tsx
+++ b/x-pack/examples/embedded_lens_example/public/mount.tsx
@@ -23,7 +23,13 @@ export const mount = (coreSetup: CoreSetup) => async ({
const defaultIndexPattern = await plugins.data.indexPatterns.getDefault();
- const reactElement = ;
+ const i18nCore = core.i18n;
+
+ const reactElement = (
+
+
+
+ );
render(reactElement, element);
return () => unmountComponentAtNode(element);
};
diff --git a/x-pack/plugins/alerting/server/health/get_state.test.ts b/x-pack/plugins/alerting/server/health/get_state.test.ts
index 96627e10fb3bd..2dddf81e3b766 100644
--- a/x-pack/plugins/alerting/server/health/get_state.test.ts
+++ b/x-pack/plugins/alerting/server/health/get_state.test.ts
@@ -58,7 +58,6 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
const mockTaskManager = taskManagerMock.createStart();
mockTaskManager.get.mockResolvedValue(getHealthCheckTask());
const pollInterval = 100;
- const halfInterval = Math.floor(pollInterval / 2);
getHealthStatusStream(
mockTaskManager,
@@ -77,16 +76,15 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
pollInterval
).subscribe();
- // shouldn't fire before poll interval passes
+ // should fire before poll interval passes
// should fire once each poll interval
- jest.advanceTimersByTime(halfInterval);
- expect(mockTaskManager.get).toHaveBeenCalledTimes(0);
- jest.advanceTimersByTime(halfInterval);
expect(mockTaskManager.get).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(pollInterval);
expect(mockTaskManager.get).toHaveBeenCalledTimes(2);
jest.advanceTimersByTime(pollInterval);
expect(mockTaskManager.get).toHaveBeenCalledTimes(3);
+ jest.advanceTimersByTime(pollInterval);
+ expect(mockTaskManager.get).toHaveBeenCalledTimes(4);
});
it('should retry on error', async () => {
@@ -94,7 +92,6 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
mockTaskManager.get.mockRejectedValue(new Error('Failure'));
const retryDelay = 10;
const pollInterval = 100;
- const halfInterval = Math.floor(pollInterval / 2);
getHealthStatusStream(
mockTaskManager,
@@ -114,28 +111,27 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
retryDelay
).subscribe();
- jest.advanceTimersByTime(halfInterval);
- expect(mockTaskManager.get).toHaveBeenCalledTimes(0);
- jest.advanceTimersByTime(halfInterval);
expect(mockTaskManager.get).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(pollInterval);
+ expect(mockTaskManager.get).toHaveBeenCalledTimes(2);
// Retry on failure
let numTimesCalled = 1;
for (let i = 0; i < MAX_RETRY_ATTEMPTS; i++) {
await tick();
jest.advanceTimersByTime(retryDelay);
- expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled++ + 1);
+ expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled++ + 2);
}
// Once we've exceeded max retries, should not try again
await tick();
jest.advanceTimersByTime(retryDelay);
- expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled);
+ expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled + 1);
// Once another poll interval passes, should call fn again
await tick();
jest.advanceTimersByTime(pollInterval - MAX_RETRY_ATTEMPTS * retryDelay);
- expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled + 1);
+ expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled + 2);
});
it('should return healthy status when health status is "ok"', async () => {
diff --git a/x-pack/plugins/alerting/server/health/get_state.ts b/x-pack/plugins/alerting/server/health/get_state.ts
index 30099614ea42b..255037d7015a2 100644
--- a/x-pack/plugins/alerting/server/health/get_state.ts
+++ b/x-pack/plugins/alerting/server/health/get_state.ts
@@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { defer, of, interval, Observable, throwError, timer } from 'rxjs';
-import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators';
+import { catchError, mergeMap, retryWhen, startWith, switchMap } from 'rxjs/operators';
import {
Logger,
SavedObjectsServiceStart,
@@ -121,6 +121,17 @@ export const getHealthStatusStream = (
retryDelay?: number
): Observable> =>
interval(healthStatusInterval ?? HEALTH_STATUS_INTERVAL).pipe(
+ // Emit an initial check
+ startWith(
+ getHealthServiceStatusWithRetryAndErrorHandling(
+ taskManager,
+ logger,
+ savedObjects,
+ config,
+ retryDelay
+ )
+ ),
+ // On each interval do a new check
switchMap(() =>
getHealthServiceStatusWithRetryAndErrorHandling(
taskManager,
diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts
index 769243b8feaf6..41ae9f15d9af9 100644
--- a/x-pack/plugins/alerting/server/plugin.ts
+++ b/x-pack/plugins/alerting/server/plugin.ts
@@ -7,7 +7,7 @@
import type { PublicMethodsOf } from '@kbn/utility-types';
import { first, map, share } from 'rxjs/operators';
-import { Observable } from 'rxjs';
+import { BehaviorSubject, Observable } from 'rxjs';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { combineLatest } from 'rxjs';
import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server';
@@ -34,6 +34,7 @@ import {
StatusServiceSetup,
ServiceStatus,
SavedObjectsBulkGetObject,
+ ServiceStatusLevels,
} from '../../../../src/core/server';
import type { AlertingRequestHandlerContext } from './types';
import { defineRoutes } from './routes';
@@ -226,17 +227,23 @@ export class AlertingPlugin {
this.config
);
+ const serviceStatus$ = new BehaviorSubject({
+ level: ServiceStatusLevels.unavailable,
+ summary: 'Alerting is initializing',
+ });
+ core.status.set(serviceStatus$);
+
core.getStartServices().then(async ([coreStart, startPlugins]) => {
- core.status.set(
- combineLatest([
- core.status.derivedStatus$,
- getHealthStatusStream(
- startPlugins.taskManager,
- this.logger,
- coreStart.savedObjects,
- this.config
- ),
- ]).pipe(
+ combineLatest([
+ core.status.derivedStatus$,
+ getHealthStatusStream(
+ startPlugins.taskManager,
+ this.logger,
+ coreStart.savedObjects,
+ this.config
+ ),
+ ])
+ .pipe(
map(([derivedStatus, healthStatus]) => {
if (healthStatus.level > derivedStatus.level) {
return healthStatus as ServiceStatus;
@@ -246,7 +253,7 @@ export class AlertingPlugin {
}),
share()
)
- );
+ .subscribe(serviceStatus$);
});
initializeAlertingHealth(this.logger, plugins.taskManager, core.getStartServices());
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
index a38e3b2a3d7c6..643653c24aeb3 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
@@ -18,7 +18,7 @@ import {
} from '@elastic/charts';
import { EuiTitle } from '@elastic/eui';
import d3 from 'd3';
-import React from 'react';
+import React, { Suspense, useState } from 'react';
import { RULE_ID } from '@kbn/rule-data-utils/target/technical_field_names';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
@@ -27,6 +27,7 @@ import { useTheme } from '../../../../hooks/use_theme';
import { AlertType } from '../../../../../common/alert_types';
import { getAlertAnnotations } from '../../../shared/charts/helper/get_alert_annotations';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
+import { LazyAlertsFlyout } from '../../../../../../observability/public';
type ErrorDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/errors/distribution'>;
@@ -68,6 +69,9 @@ export function ErrorDistribution({ distribution, title }: Props) {
const { observabilityRuleTypeRegistry } = useApmPluginContext();
const { alerts } = useApmServiceContext();
const { getFormatter } = observabilityRuleTypeRegistry;
+ const [selectedAlertId, setSelectedAlertId] = useState(
+ undefined
+ );
const tooltipProps: SettingsSpec['tooltip'] = {
headerFormatter: (tooltip: TooltipValue) => {
@@ -122,8 +126,21 @@ export function ErrorDistribution({ distribution, title }: Props) {
),
chartStartTime: buckets[0].x0,
getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
})}
+
+ {
+ setSelectedAlertId(undefined);
+ }}
+ selectedAlertId={selectedAlertId}
+ />
+
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
index 6d04996b5f24c..20d930d28599f 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
@@ -55,26 +55,43 @@ export function UXActionMenu({
http?.basePath.get()
);
+ const kibana = useKibana();
+
return (
{ANALYZE_MESSAGE}
}>
{ANALYZE_DATA}
+
+
+ {i18n.translate('xpack.apm.addDataButtonLabel', {
+ defaultMessage: 'Add data',
+ })}
+
+
);
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
index add6ac1b08b28..c525a71ea4589 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
@@ -24,7 +24,7 @@ export function ClientMetrics() {
} = useUrlParams();
return (
-
+
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx
index 175b40f85d64b..b696a46f59bd1 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx
@@ -11,7 +11,7 @@ import { JSErrors } from './JSErrors';
export function ImpactfulMetrics() {
return (
-
+
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
index a8435beca1e4a..adfb45303a4f3 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
@@ -89,7 +89,7 @@ export function PageLoadDistribution() {
{
[`${serviceName}-page-views`]: {
dataType: 'ux',
- reportType: 'pld',
+ reportType: 'dist',
time: { from: rangeFrom!, to: rangeTo! },
reportDefinitions: {
'service.name': serviceName as string[],
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/PageLoadAndViews.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/PageLoadAndViews.tsx
index b51e2559b7f15..9dd83fd1c8fd1 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/PageLoadAndViews.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/PageLoadAndViews.tsx
@@ -14,12 +14,12 @@ export function PageLoadAndViews() {
return (
-
+
-
+
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/VisitorBreakdowns.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/VisitorBreakdowns.tsx
index 0433988ecfa21..ff79feaa924f3 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/VisitorBreakdowns.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/VisitorBreakdowns.tsx
@@ -14,12 +14,12 @@ export function VisitorBreakdownsPanel() {
return (
-
+
-
+
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx
index 44212fed98987..a665b6560c7e9 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx
@@ -62,7 +62,7 @@ export function UXMetrics() {
);
return (
-
+
diff --git a/x-pack/plugins/apm/public/components/app/service_map/index.tsx b/x-pack/plugins/apm/public/components/app/service_map/index.tsx
index df8438c5c80a4..582eafe7553af 100644
--- a/x-pack/plugins/apm/public/components/app/service_map/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_map/index.tsx
@@ -146,7 +146,7 @@ export function ServiceMap({
return (
<>
-
+
{data.charts.map((chart) => (
-
+
) : (
-
+
+
+
)}
@@ -171,7 +173,7 @@ export function ServiceNodeMetrics({
{data.charts.map((chart) => (
-
+
-
+
@@ -63,7 +63,7 @@ export function ServiceOverview({ serviceName }: ServiceOverviewProps) {
-
+
@@ -84,7 +84,7 @@ export function ServiceOverview({ serviceName }: ServiceOverviewProps) {
)}
-
+
@@ -101,7 +101,7 @@ export function ServiceOverview({ serviceName }: ServiceOverviewProps) {
{!isRumAgent && (
-
+
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx
index 8513e0835d373..719409b0f97ff 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx
@@ -228,7 +228,7 @@ export function ServiceOverviewInstancesChartAndTable({
/>
-
+
+
{i18n.translate('xpack.apm.serviceOverview.throughtputChartTitle', {
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx
index 4017495dd3b5d..11a0cc1234f42 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx
@@ -45,7 +45,7 @@ export const MaybeViewTraceLink = ({
}
)}
>
-
+
{viewFullTraceButtonLabel}
@@ -67,7 +67,7 @@ export const MaybeViewTraceLink = ({
}
)}
>
-
+
{viewFullTraceButtonLabel}
@@ -92,7 +92,9 @@ export const MaybeViewTraceLink = ({
environment={nextEnvironment}
latencyAggregationType={latencyAggregationType}
>
- {viewFullTraceButtonLabel}
+
+ {viewFullTraceButtonLabel}
+
);
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/index.tsx
index 3d3ce3262f13b..6f5b95b103f6b 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/index.tsx
@@ -83,13 +83,13 @@ export function WaterfallWithSummmary({
/>
);
- return
{content} ;
+ return
{content} ;
}
const entryTransaction = entryWaterfallTransaction.doc;
return (
-
+
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx
index b9508b2d303a2..3cac05ba2d96a 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx
@@ -76,11 +76,13 @@ export function TransactionDetails() {
return (
<>
+
+
{transactionName}
-
+
@@ -88,7 +90,7 @@ export function TransactionDetails() {
-
+
-
+
Transactions
diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap b/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap
index 8e89939f585aa..87b5b68e26026 100644
--- a/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap
+++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap
@@ -4,7 +4,7 @@ exports[`ImpactBar component should render with default values 1`] = `
`;
diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx
index 92f488b8ba0ee..87b3c669e993c 100644
--- a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx
@@ -18,7 +18,7 @@ export interface ImpactBarProps extends Record {
export function ImpactBar({
value,
- size = 'l',
+ size = 'm',
max = 100,
color = 'primary',
...rest
diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx
index 95acc55196c54..5b4f4e24af44d 100644
--- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx
+++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx
@@ -66,7 +66,8 @@ export function AlertingPopoverAndFlyout({
const button = (
setPopoverOpen((prevState) => !prevState)}
diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx
index ade49bc7e3aa4..28c000310346d 100644
--- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx
+++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx
@@ -42,14 +42,15 @@ export function AnomalyDetectionSetupLink() {
return (
{canGetJobs && hasValidLicense ? (
) : (
-
+
)}
{ANOMALY_DETECTION_LINK_LABEL}
@@ -64,7 +65,7 @@ export function MissingJobsAlert({ environment }: { environment?: string }) {
anomalyDetectionJobsStatus,
} = useAnomalyDetectionJobsContext();
- const defaultIcon = ;
+ const defaultIcon = ;
if (anomalyDetectionJobsStatus === FETCH_STATUS.LOADING) {
return ;
@@ -92,7 +93,7 @@ export function MissingJobsAlert({ environment }: { environment?: string }) {
return (
-
+
);
}
diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx
index 134941990a0f4..86f0d3fde1cd5 100644
--- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx
@@ -40,16 +40,13 @@ export function ApmHeaderActionMenu() {
}
return (
-
-
+
+
{i18n.translate('xpack.apm.settingsLinkLabel', {
defaultMessage: 'Settings',
})}
+ {canAccessML && }
{isAlertingAvailable && (
)}
- {canAccessML && }
() =>
link: '/',
reason: 'a good reason',
});
+const selectedAlertId = undefined;
+const setSelectedAlertId = jest.fn();
describe('getAlertAnnotations', () => {
describe('with no alerts', () => {
it('returns an empty array', () => {
expect(
- getAlertAnnotations({ alerts: [], chartStartTime, getFormatter, theme })
+ getAlertAnnotations({
+ alerts: [],
+ chartStartTime,
+ getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
+ theme,
+ })
).toEqual([]);
});
});
@@ -66,6 +75,8 @@ describe('getAlertAnnotations', () => {
alerts: [alert],
chartStartTime,
getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
})![0].props.style.line.stroke
).toEqual(euiColorDanger);
@@ -77,6 +88,8 @@ describe('getAlertAnnotations', () => {
alerts: [alert],
chartStartTime,
getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
})![0].props.dataValues[0].header
).toEqual('Alert');
@@ -88,6 +101,8 @@ describe('getAlertAnnotations', () => {
alerts: [alert],
chartStartTime,
getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
})![0].props.dataValues[0].details
).toEqual('a good reason');
@@ -103,6 +118,8 @@ describe('getAlertAnnotations', () => {
alerts: [alert],
chartStartTime,
getFormatter: getNoFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
})![0].props.dataValues[0].details
).toEqual(alert['rule.name']![0]);
@@ -118,6 +135,8 @@ describe('getAlertAnnotations', () => {
alerts: [alert],
chartStartTime: beforeChartStartTime,
getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
})![0].props.dataValues[0].dataValue
).toEqual(beforeChartStartTime);
@@ -137,6 +156,8 @@ describe('getAlertAnnotations', () => {
alerts: [warningAlert],
chartStartTime,
getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
})![0].props.style.line.stroke
).toEqual(euiColorWarning);
@@ -148,6 +169,8 @@ describe('getAlertAnnotations', () => {
alerts: [warningAlert],
chartStartTime,
getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
})![0].props.dataValues[0].header
).toEqual('Warning Alert');
@@ -166,6 +189,8 @@ describe('getAlertAnnotations', () => {
alerts: [criticalAlert],
chartStartTime,
getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
})![0].props.style.line.stroke
).toEqual(euiColorDanger);
@@ -177,6 +202,8 @@ describe('getAlertAnnotations', () => {
alerts: [criticalAlert],
chartStartTime,
getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
})![0].props.dataValues[0].header
).toEqual('Critical Alert');
diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx
index 2a2268b598efa..fa0725018f783 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx
@@ -10,7 +10,7 @@ import {
Position,
RectAnnotation,
} from '@elastic/charts';
-import { EuiIcon } from '@elastic/eui';
+import { EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
ALERT_DURATION,
@@ -20,7 +20,7 @@ import {
RULE_ID,
RULE_NAME,
} from '@kbn/rule-data-utils/target/technical_field_names';
-import React from 'react';
+import React, { Dispatch, SetStateAction } from 'react';
import { EuiTheme } from 'src/plugins/kibana_react/common';
import { ValuesType } from 'utility-types';
import type { ObservabilityRuleTypeRegistry } from '../../../../../../observability/public';
@@ -68,15 +68,30 @@ function getAlertHeader({
}
}
+/**
+ * Get the components needed to render alert annotations.
+ *
+ * You might be thinking, "Hey, this is a function that returns DOM.
+ * This should not be a function but a component."
+ *
+ * You would be correct, except for https://github.com/elastic/elastic-charts/issues/914,
+ * which makes it so if you construct a chart with its elements broken into
+ * different components it makes the whole chart disappear, which is not what
+ * we want.
+ */
export function getAlertAnnotations({
alerts,
chartStartTime,
getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
}: {
alerts?: Alert[];
chartStartTime: number;
getFormatter: ObservabilityRuleTypeRegistry['getFormatter'];
+ selectedAlertId?: string;
+ setSelectedAlertId: Dispatch>;
theme: EuiTheme;
}) {
return alerts?.flatMap((alert) => {
@@ -100,6 +115,7 @@ export function getAlertAnnotations({
formatters: { asDuration, asPercent },
}) ?? {}),
};
+ const isSelected = uuid === selectedAlertId;
return [
}
+ marker={
+ {
+ if (selectedAlertId === uuid) {
+ setSelectedAlertId(undefined);
+ } else {
+ setSelectedAlertId(uuid);
+ }
+ }}
+ iconSize={isSelected ? 'l' : 'm'}
+ iconType="alert"
+ size="xs"
+ />
+ }
markerPosition={Position.Top}
- style={{ line: { opacity: 1, strokeWidth: 2, stroke: color } }}
+ style={{
+ line: { opacity: 1, strokeWidth: isSelected ? 6 : 2, stroke: color },
+ }}
/>,
,
];
});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx
index ce4f36ced7903..0ad4be17e35cb 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx
@@ -104,7 +104,7 @@ export function InstancesLatencyDistributionChart({
};
return (
-
+
{i18n.translate('xpack.apm.instancesLatencyDistributionChartTitle', {
diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx
index 0dd7da02001d7..d1dcd831eadd7 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx
@@ -9,6 +9,7 @@ import { StoryContext } from '@storybook/react';
import React, { ComponentType } from 'react';
import { MemoryRouter, Route } from 'react-router-dom';
import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common';
+import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
import {
ApmPluginContext,
@@ -48,6 +49,7 @@ export default {
toasts: { addWarning: () => {}, addDanger: () => {} },
},
http: {
+ basePath: { prepend: () => {} },
get: (endpoint: string) => {
switch (endpoint) {
case '/api/apm/services/test-service/transactions/charts/latency':
@@ -59,7 +61,7 @@ export default {
}
},
},
- uiSettings: { get: () => true },
+ uiSettings: { get: () => '' },
},
plugins: { observability: { isAlertingExperienceEnabled: () => true } },
observabilityRuleTypeRegistry: { getFormatter: () => undefined },
@@ -71,20 +73,24 @@ export default {
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx
index 49ef146f4b511..9e7a3ac744ffe 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx
@@ -24,9 +24,12 @@ import {
} from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import React from 'react';
+import React, { Suspense, useState } from 'react';
import { useHistory } from 'react-router-dom';
-import { useChartTheme } from '../../../../../observability/public';
+import {
+ LazyAlertsFlyout,
+ useChartTheme,
+} from '../../../../../observability/public';
import { asAbsoluteDateTime } from '../../../../common/utils/formatters';
import {
Coordinate,
@@ -88,6 +91,9 @@ export function TimeseriesChart({
const { setPointerEvent, chartRef } = useChartPointerEventContext();
const theme = useTheme();
const chartTheme = useChartTheme();
+ const [selectedAlertId, setSelectedAlertId] = useState(
+ undefined
+ );
const xValues = timeseries.flatMap(({ data }) => data.map(({ x }) => x));
@@ -208,8 +214,21 @@ export function TimeseriesChart({
alerts,
chartStartTime: xValues[0],
getFormatter,
+ selectedAlertId,
+ setSelectedAlertId,
theme,
})}
+
+ {
+ setSelectedAlertId(undefined);
+ }}
+ selectedAlertId={selectedAlertId}
+ />
+
);
diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx
index 978604c4c96ec..40c5e39589fb1 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx
@@ -22,7 +22,7 @@ export function TransactionBreakdownChart({
const { timeseries } = data;
return (
-
+
diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx
index 3f868ae272e3a..019a25b1e9ed3 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx
@@ -37,13 +37,13 @@ export function TransactionCharts() {
-
+
-
+
{i18n.translate(
diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx
index 7eceaf5ca8e5d..96cb7c49a6710 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx
@@ -135,7 +135,7 @@ export function TransactionErrorRateChart({
];
return (
-
+
{i18n.translate('xpack.apm.errorRate', {
diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/alert_details.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/alert_details.tsx
deleted file mode 100644
index 9f6378ccb4497..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/service_icons/alert_details.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-import React from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
-import { parse, format } from 'url';
-import { uniqBy } from 'lodash';
-import {
- ALERT_ID,
- ALERT_START,
- RULE_ID,
- RULE_NAME,
-} from '@kbn/rule-data-utils/target/technical_field_names';
-import { parseTechnicalFields } from '../../../../../rule_registry/common';
-import { useUrlParams } from '../../../context/url_params_context/use_url_params';
-import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
-import { APIReturnType } from '../../../services/rest/createCallApmApi';
-import { asPercent, asDuration } from '../../../../common/utils/formatters';
-import { TimestampTooltip } from '../TimestampTooltip';
-
-interface AlertDetailProps {
- alerts: APIReturnType<'GET /api/apm/services/{serviceName}/alerts'>['alerts'];
-}
-
-export function AlertDetails({ alerts }: AlertDetailProps) {
- const {
- observabilityRuleTypeRegistry,
- core: {
- http: {
- basePath: { prepend },
- },
- },
- } = useApmPluginContext();
-
- const {
- urlParams: { rangeFrom, rangeTo },
- } = useUrlParams();
-
- const collapsedAlerts = uniqBy(alerts, (alert) => alert[ALERT_ID]![0]!).map(
- (alert) => {
- return parseTechnicalFields(alert);
- }
- );
-
- return (
-
- {collapsedAlerts.map((alert) => {
- const formatter = observabilityRuleTypeRegistry.getFormatter(
- alert[RULE_ID]!
- );
- const formatted = {
- link: undefined,
- reason: alert[RULE_NAME],
- ...(formatter?.({
- fields: alert,
- formatters: { asDuration, asPercent },
- }) ?? {}),
- };
-
- const parsedLink = formatted.link
- ? parse(formatted.link, true)
- : undefined;
-
- return (
-
-
-
- {parsedLink ? (
-
- {formatted.reason}
-
- ) : (
- formatted.reason
- )}
-
-
-
-
-
-
- );
- })}
-
- );
-}
diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx
index d64605da2bc3f..7889320342902 100644
--- a/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx
@@ -8,7 +8,6 @@
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { ReactChild, useState } from 'react';
-import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useTheme } from '../../../hooks/use_theme';
import { ContainerType } from '../../../../common/service_metadata';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
@@ -18,7 +17,6 @@ import { CloudDetails } from './cloud_details';
import { ContainerDetails } from './container_details';
import { IconPopover } from './icon_popover';
import { ServiceDetails } from './service_details';
-import { AlertDetails } from './alert_details';
interface Props {
serviceName: string;
@@ -73,8 +71,6 @@ export function ServiceIcons({ serviceName }: Props) {
const theme = useTheme();
- const { alerts } = useApmServiceContext();
-
const { data: icons, status: iconsFetchStatus } = useFetcher(
(callApmApi) => {
if (serviceName && start && end) {
@@ -146,19 +142,6 @@ export function ServiceIcons({ serviceName }: Props) {
}),
component: ,
},
- {
- key: 'alerts',
- icon: {
- type: 'bell',
- color: theme.eui.euiColorDanger,
- size: 'm',
- },
- isVisible: alerts.length > 0,
- title: i18n.translate('xpack.apm.serviceIcons.alerts', {
- defaultMessage: 'Alerts',
- }),
- component: ,
- },
];
return (
diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts b/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts
index bebfcba1a93b4..e2e1391a2f842 100644
--- a/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts
+++ b/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts
@@ -6,7 +6,6 @@
*/
import { maybe } from '../../../../common/utils/maybe';
-import awsIcon from './icons/aws.svg';
import cassandraIcon from './icons/cassandra.svg';
import databaseIcon from './icons/database.svg';
import defaultIcon from './icons/default.svg';
@@ -33,12 +32,14 @@ const defaultTypeIcons: { [key: string]: string } = {
resource: globeIcon,
};
-const typeIcons: { [key: string]: { [key: string]: string } } = {
+export const typeIcons: { [type: string]: { [subType: string]: string } } = {
aws: {
- servicename: awsIcon,
+ servicename: 'logoAWS',
},
db: {
cassandra: cassandraIcon,
+ cosmosdb: 'logoAzure',
+ dynamodb: 'logoAWS',
elasticsearch: elasticsearchIcon,
mongodb: mongodbIcon,
mysql: mysqlIcon,
@@ -51,8 +52,18 @@ const typeIcons: { [key: string]: { [key: string]: string } } = {
websocket: websocketIcon,
},
messaging: {
+ azurequeue: 'logoAzure',
+ azureservicebus: 'logoAzure',
jms: javaIcon,
kafka: kafkaIcon,
+ sns: 'logoAWS',
+ sqs: 'logoAWS',
+ },
+ storage: {
+ azureblob: 'logoAzure',
+ azurefile: 'logoAzure',
+ azuretable: 'logoAzure',
+ s3: 'logoAWS',
},
template: {
handlebars: handlebarsIcon,
diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx b/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx
new file mode 100644
index 0000000000000..951d3e61f1846
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ EuiFlexGrid,
+ EuiFlexItem,
+ EuiCopy,
+ EuiPanel,
+ EuiSpacer,
+ EuiCodeBlock,
+} from '@elastic/eui';
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common';
+import { SpanIcon } from './index';
+import { typeIcons } from './get_span_icon';
+
+const types = Object.keys(typeIcons);
+
+storiesOf('shared/span_icon/span_icon', module)
+ .addDecorator((storyFn) => {storyFn()} )
+ .add(
+ 'Span icon',
+ () => {
+ return (
+ <>
+
+ {' '}
+
+
+
+
+ {types.map((type) => {
+ const subTypes = Object.keys(typeIcons[type]);
+ return (
+ <>
+ {subTypes.map((subType) => {
+ const id = `${type}.${subType}`;
+ return (
+
+
+ {(copy) => (
+
+ {' '}
+ {id}
+
+ )}
+
+
+ );
+ })}
+ >
+ );
+ })}
+
+ >
+ );
+ },
+ {}
+ );
diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts
index f8c8ffb855080..40c424c6a4284 100644
--- a/x-pack/plugins/apm/public/plugin.ts
+++ b/x-pack/plugins/apm/public/plugin.ts
@@ -106,7 +106,7 @@ export class ApmPlugin implements Plugin {
// APM navigation
{
label: 'APM',
- sortKey: 200,
+ sortKey: 400,
entries: [
{ label: servicesTitle, app: 'apm', path: '/services' },
{ label: tracesTitle, app: 'apm', path: '/traces' },
@@ -117,7 +117,7 @@ export class ApmPlugin implements Plugin {
// UX navigation
{
label: 'User Experience',
- sortKey: 201,
+ sortKey: 600,
entries: [
{
label: i18n.translate('xpack.apm.ux.overview.heading', {
diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts
index 091982598d6a3..6ce175fcb8362 100644
--- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts
@@ -15,82 +15,82 @@ import {
import { ProcessorEvent } from '../../../../common/processor_event';
import { environmentQuery, rangeQuery } from '../../../../server/utils/queries';
import { AlertParams } from '../../../routes/alerts/chart_preview';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { getBucketSize } from '../../helpers/get_bucket_size';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
-export function getTransactionDurationChartPreview({
+export async function getTransactionDurationChartPreview({
alertParams,
setup,
}: {
alertParams: AlertParams;
setup: Setup & SetupTimeRange;
}) {
- return withApmSpan('get_transaction_duration_chart_preview', async () => {
- const { apmEventClient, start, end } = setup;
- const {
- aggregationType,
- environment,
- serviceName,
- transactionType,
- } = alertParams;
+ const { apmEventClient, start, end } = setup;
+ const {
+ aggregationType,
+ environment,
+ serviceName,
+ transactionType,
+ } = alertParams;
- const query = {
- bool: {
- filter: [
- { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
- ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []),
- ...(transactionType
- ? [{ term: { [TRANSACTION_TYPE]: transactionType } }]
- : []),
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ] as QueryDslQueryContainer[],
- },
- };
+ const query = {
+ bool: {
+ filter: [
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
+ ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []),
+ ...(transactionType
+ ? [{ term: { [TRANSACTION_TYPE]: transactionType } }]
+ : []),
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ] as QueryDslQueryContainer[],
+ },
+ };
- const { intervalString } = getBucketSize({ start, end, numBuckets: 20 });
+ const { intervalString } = getBucketSize({ start, end, numBuckets: 20 });
- const aggs = {
- timeseries: {
- date_histogram: {
- field: '@timestamp',
- fixed_interval: intervalString,
- },
- aggs: {
- agg:
- aggregationType === 'avg'
- ? { avg: { field: TRANSACTION_DURATION } }
- : {
- percentiles: {
- field: TRANSACTION_DURATION,
- percents: [aggregationType === '95th' ? 95 : 99],
- },
+ const aggs = {
+ timeseries: {
+ date_histogram: {
+ field: '@timestamp',
+ fixed_interval: intervalString,
+ },
+ aggs: {
+ agg:
+ aggregationType === 'avg'
+ ? { avg: { field: TRANSACTION_DURATION } }
+ : {
+ percentiles: {
+ field: TRANSACTION_DURATION,
+ percents: [aggregationType === '95th' ? 95 : 99],
},
- },
+ },
},
- };
- const params = {
- apm: { events: [ProcessorEvent.transaction] },
- body: { size: 0, query, aggs },
- };
- const resp = await apmEventClient.search(params);
+ },
+ };
+ const params = {
+ apm: { events: [ProcessorEvent.transaction] },
+ body: { size: 0, query, aggs },
+ };
+ const resp = await apmEventClient.search(
+ 'get_transaction_duration_chart_preview',
+ params
+ );
- if (!resp.aggregations) {
- return [];
- }
+ if (!resp.aggregations) {
+ return [];
+ }
- return resp.aggregations.timeseries.buckets.map((bucket) => {
- const percentilesKey = aggregationType === '95th' ? '95.0' : '99.0';
- const x = bucket.key;
- const y =
- aggregationType === 'avg'
- ? (bucket.agg as { value: number | null }).value
- : (bucket.agg as { values: Record }).values[
- percentilesKey
- ];
+ return resp.aggregations.timeseries.buckets.map((bucket) => {
+ const percentilesKey = aggregationType === '95th' ? '95.0' : '99.0';
+ const x = bucket.key;
+ const y =
+ aggregationType === 'avg'
+ ? (bucket.agg as { value: number | null }).value
+ : (bucket.agg as { values: Record }).values[
+ percentilesKey
+ ];
- return { x, y };
- });
+ return { x, y };
});
}
diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts
index 2cf1317dc44b0..3d64c63cb2041 100644
--- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts
@@ -9,58 +9,58 @@ import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../../common/processor_event';
import { AlertParams } from '../../../routes/alerts/chart_preview';
import { environmentQuery, rangeQuery } from '../../../../server/utils/queries';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { getBucketSize } from '../../helpers/get_bucket_size';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
-export function getTransactionErrorCountChartPreview({
+export async function getTransactionErrorCountChartPreview({
setup,
alertParams,
}: {
setup: Setup & SetupTimeRange;
alertParams: AlertParams;
}) {
- return withApmSpan('get_transaction_error_count_chart_preview', async () => {
- const { apmEventClient, start, end } = setup;
- const { serviceName, environment } = alertParams;
+ const { apmEventClient, start, end } = setup;
+ const { serviceName, environment } = alertParams;
- const query = {
- bool: {
- filter: [
- ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []),
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ],
- },
- };
+ const query = {
+ bool: {
+ filter: [
+ ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []),
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ],
+ },
+ };
- const { intervalString } = getBucketSize({ start, end, numBuckets: 20 });
+ const { intervalString } = getBucketSize({ start, end, numBuckets: 20 });
- const aggs = {
- timeseries: {
- date_histogram: {
- field: '@timestamp',
- fixed_interval: intervalString,
- },
+ const aggs = {
+ timeseries: {
+ date_histogram: {
+ field: '@timestamp',
+ fixed_interval: intervalString,
},
- };
+ },
+ };
- const params = {
- apm: { events: [ProcessorEvent.error] },
- body: { size: 0, query, aggs },
- };
+ const params = {
+ apm: { events: [ProcessorEvent.error] },
+ body: { size: 0, query, aggs },
+ };
- const resp = await apmEventClient.search(params);
+ const resp = await apmEventClient.search(
+ 'get_transaction_error_count_chart_preview',
+ params
+ );
- if (!resp.aggregations) {
- return [];
- }
+ if (!resp.aggregations) {
+ return [];
+ }
- return resp.aggregations.timeseries.buckets.map((bucket) => {
- return {
- x: bucket.key,
- y: bucket.doc_count,
- };
- });
+ return resp.aggregations.timeseries.buckets.map((bucket) => {
+ return {
+ x: bucket.key,
+ y: bucket.doc_count,
+ };
});
}
diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts
index f0c8d23e0e8fa..0a6a25ad9c533 100644
--- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts
@@ -64,7 +64,10 @@ export async function getTransactionErrorRateChartPreview({
body: { size: 0, query, aggs },
};
- const resp = await apmEventClient.search(params);
+ const resp = await apmEventClient.search(
+ 'get_transaction_error_rate_chart_preview',
+ params
+ );
if (!resp.aggregations) {
return [];
diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts
index 8ee469c9a93c7..11e9f99ddb356 100644
--- a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts
+++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts
@@ -21,68 +21,68 @@ import {
getTimeseriesAggregation,
getTransactionErrorRateTimeSeries,
} from '../../helpers/transaction_error_rate';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters';
interface Options extends CorrelationsOptions {
fieldNames: string[];
}
export async function getCorrelationsForFailedTransactions(options: Options) {
- return withApmSpan('get_correlations_for_failed_transactions', async () => {
- const { fieldNames, setup } = options;
- const { apmEventClient } = setup;
- const filters = getCorrelationsFilters(options);
-
- const params = {
- apm: { events: [ProcessorEvent.transaction] },
- track_total_hits: true,
- body: {
- size: 0,
- query: {
- bool: { filter: filters },
- },
- aggs: {
- failed_transactions: {
- filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } },
-
- // significant term aggs
- aggs: fieldNames.reduce((acc, fieldName) => {
- return {
- ...acc,
- [fieldName]: {
- significant_terms: {
- size: 10,
- field: fieldName,
- background_filter: {
- bool: {
- filter: filters,
- must_not: {
- term: { [EVENT_OUTCOME]: EventOutcome.failure },
- },
+ const { fieldNames, setup } = options;
+ const { apmEventClient } = setup;
+ const filters = getCorrelationsFilters(options);
+
+ const params = {
+ apm: { events: [ProcessorEvent.transaction] },
+ track_total_hits: true,
+ body: {
+ size: 0,
+ query: {
+ bool: { filter: filters },
+ },
+ aggs: {
+ failed_transactions: {
+ filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } },
+
+ // significant term aggs
+ aggs: fieldNames.reduce((acc, fieldName) => {
+ return {
+ ...acc,
+ [fieldName]: {
+ significant_terms: {
+ size: 10,
+ field: fieldName,
+ background_filter: {
+ bool: {
+ filter: filters,
+ must_not: {
+ term: { [EVENT_OUTCOME]: EventOutcome.failure },
},
},
},
},
- };
- }, {} as Record),
- },
+ },
+ };
+ }, {} as Record),
},
},
- };
-
- const response = await apmEventClient.search(params);
- if (!response.aggregations) {
- return { significantTerms: [] };
- }
-
- const sigTermAggs = omit(
- response.aggregations?.failed_transactions,
- 'doc_count'
- );
-
- const topSigTerms = processSignificantTermAggs({ sigTermAggs });
- return getErrorRateTimeSeries({ setup, filters, topSigTerms });
- });
+ },
+ };
+
+ const response = await apmEventClient.search(
+ 'get_correlations_for_failed_transactions',
+ params
+ );
+ if (!response.aggregations) {
+ return { significantTerms: [] };
+ }
+
+ const sigTermAggs = omit(
+ response.aggregations?.failed_transactions,
+ 'doc_count'
+ );
+
+ const topSigTerms = processSignificantTermAggs({ sigTermAggs });
+ return getErrorRateTimeSeries({ setup, filters, topSigTerms });
}
export async function getErrorRateTimeSeries({
@@ -94,58 +94,59 @@ export async function getErrorRateTimeSeries({
filters: ESFilter[];
topSigTerms: TopSigTerm[];
}) {
- return withApmSpan('get_error_rate_timeseries', async () => {
- const { start, end, apmEventClient } = setup;
- const { intervalString } = getBucketSize({ start, end, numBuckets: 15 });
-
- if (isEmpty(topSigTerms)) {
- return { significantTerms: [] };
+ const { start, end, apmEventClient } = setup;
+ const { intervalString } = getBucketSize({ start, end, numBuckets: 15 });
+
+ if (isEmpty(topSigTerms)) {
+ return { significantTerms: [] };
+ }
+
+ const timeseriesAgg = getTimeseriesAggregation(start, end, intervalString);
+
+ const perTermAggs = topSigTerms.reduce(
+ (acc, term, index) => {
+ acc[`term_${index}`] = {
+ filter: { term: { [term.fieldName]: term.fieldValue } },
+ aggs: { timeseries: timeseriesAgg },
+ };
+ return acc;
+ },
+ {} as {
+ [key: string]: {
+ filter: AggregationOptionsByType['filter'];
+ aggs: { timeseries: typeof timeseriesAgg };
+ };
}
-
- const timeseriesAgg = getTimeseriesAggregation(start, end, intervalString);
-
- const perTermAggs = topSigTerms.reduce(
- (acc, term, index) => {
- acc[`term_${index}`] = {
- filter: { term: { [term.fieldName]: term.fieldValue } },
- aggs: { timeseries: timeseriesAgg },
- };
- return acc;
- },
- {} as {
- [key: string]: {
- filter: AggregationOptionsByType['filter'];
- aggs: { timeseries: typeof timeseriesAgg };
- };
- }
- );
-
- const params = {
- // TODO: add support for metrics
- apm: { events: [ProcessorEvent.transaction] },
- body: {
- size: 0,
- query: { bool: { filter: filters } },
- aggs: perTermAggs,
- },
- };
-
- const response = await apmEventClient.search(params);
- const { aggregations } = response;
-
- if (!aggregations) {
- return { significantTerms: [] };
- }
-
- return {
- significantTerms: topSigTerms.map((topSig, index) => {
- const agg = aggregations[`term_${index}`]!;
-
- return {
- ...topSig,
- timeseries: getTransactionErrorRateTimeSeries(agg.timeseries.buckets),
- };
- }),
- };
- });
+ );
+
+ const params = {
+ // TODO: add support for metrics
+ apm: { events: [ProcessorEvent.transaction] },
+ body: {
+ size: 0,
+ query: { bool: { filter: filters } },
+ aggs: perTermAggs,
+ },
+ };
+
+ const response = await apmEventClient.search(
+ 'get_error_rate_timeseries',
+ params
+ );
+ const { aggregations } = response;
+
+ if (!aggregations) {
+ return { significantTerms: [] };
+ }
+
+ return {
+ significantTerms: topSigTerms.map((topSig, index) => {
+ const agg = aggregations[`term_${index}`]!;
+
+ return {
+ ...topSig,
+ timeseries: getTransactionErrorRateTimeSeries(agg.timeseries.buckets),
+ };
+ }),
+ };
}
diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts
index 9387e64a51e01..f3477273806b6 100644
--- a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts
+++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts
@@ -11,41 +11,41 @@ import {
getTimeseriesAggregation,
getTransactionErrorRateTimeSeries,
} from '../../helpers/transaction_error_rate';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters';
export async function getOverallErrorTimeseries(options: CorrelationsOptions) {
- return withApmSpan('get_error_rate_timeseries', async () => {
- const { setup } = options;
- const filters = getCorrelationsFilters(options);
- const { start, end, apmEventClient } = setup;
- const { intervalString } = getBucketSize({ start, end, numBuckets: 15 });
+ const { setup } = options;
+ const filters = getCorrelationsFilters(options);
+ const { start, end, apmEventClient } = setup;
+ const { intervalString } = getBucketSize({ start, end, numBuckets: 15 });
- const params = {
- // TODO: add support for metrics
- apm: { events: [ProcessorEvent.transaction] },
- body: {
- size: 0,
- query: { bool: { filter: filters } },
- aggs: {
- timeseries: getTimeseriesAggregation(start, end, intervalString),
- },
+ const params = {
+ // TODO: add support for metrics
+ apm: { events: [ProcessorEvent.transaction] },
+ body: {
+ size: 0,
+ query: { bool: { filter: filters } },
+ aggs: {
+ timeseries: getTimeseriesAggregation(start, end, intervalString),
},
- };
+ },
+ };
- const response = await apmEventClient.search(params);
- const { aggregations } = response;
+ const response = await apmEventClient.search(
+ 'get_error_rate_timeseries',
+ params
+ );
+ const { aggregations } = response;
- if (!aggregations) {
- return { overall: null };
- }
+ if (!aggregations) {
+ return { overall: null };
+ }
- return {
- overall: {
- timeseries: getTransactionErrorRateTimeSeries(
- aggregations.timeseries.buckets
- ),
- },
- };
- });
+ return {
+ overall: {
+ timeseries: getTransactionErrorRateTimeSeries(
+ aggregations.timeseries.buckets
+ ),
+ },
+ };
}
diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts
index 0f93d1411a001..c37b3e3ab8242 100644
--- a/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts
+++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts
@@ -41,60 +41,63 @@ export async function getCorrelationsForSlowTransactions(options: Options) {
return { significantTerms: [] };
}
- const response = await withApmSpan('get_significant_terms', () => {
- const params = {
- apm: { events: [ProcessorEvent.transaction] },
- body: {
- size: 0,
- query: {
- bool: {
- // foreground filters
- filter: filters,
- must: {
- function_score: {
- query: {
- range: {
- [TRANSACTION_DURATION]: { gte: durationForPercentile },
- },
+ const params = {
+ apm: { events: [ProcessorEvent.transaction] },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ // foreground filters
+ filter: filters,
+ must: {
+ function_score: {
+ query: {
+ range: {
+ [TRANSACTION_DURATION]: { gte: durationForPercentile },
},
- script_score: {
- script: {
- source: `Math.log(2 + doc['${TRANSACTION_DURATION}'].value)`,
- },
+ },
+ script_score: {
+ script: {
+ source: `Math.log(2 + doc['${TRANSACTION_DURATION}'].value)`,
},
},
},
},
},
- aggs: fieldNames.reduce((acc, fieldName) => {
- return {
- ...acc,
- [fieldName]: {
- significant_terms: {
- size: 10,
- field: fieldName,
- background_filter: {
- bool: {
- filter: [
- ...filters,
- {
- range: {
- [TRANSACTION_DURATION]: {
- lt: durationForPercentile,
- },
+ },
+ aggs: fieldNames.reduce((acc, fieldName) => {
+ return {
+ ...acc,
+ [fieldName]: {
+ significant_terms: {
+ size: 10,
+ field: fieldName,
+ background_filter: {
+ bool: {
+ filter: [
+ ...filters,
+ {
+ range: {
+ [TRANSACTION_DURATION]: {
+ lt: durationForPercentile,
},
},
- ],
- },
+ },
+ ],
},
},
},
- };
- }, {} as Record),
- },
- };
- return apmEventClient.search(params);
- });
+ },
+ };
+ }, {} as Record),
+ },
+ };
+
+ const response = await apmEventClient.search(
+ 'get_significant_terms',
+ params
+ );
+
if (!response.aggregations) {
return { significantTerms: [] };
}
diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts
index 43c261743861d..a686980700d83 100644
--- a/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts
+++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts
@@ -8,7 +8,6 @@
import { ESFilter } from '../../../../../../../typings/elasticsearch';
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../../common/processor_event';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
export async function getDurationForPercentile({
@@ -20,31 +19,27 @@ export async function getDurationForPercentile({
filters: ESFilter[];
setup: Setup & SetupTimeRange;
}) {
- return withApmSpan('get_duration_for_percentiles', async () => {
- const { apmEventClient } = setup;
- const res = await apmEventClient.search({
- apm: {
- events: [ProcessorEvent.transaction],
+ const { apmEventClient } = setup;
+ const res = await apmEventClient.search('get_duration_for_percentiles', {
+ apm: {
+ events: [ProcessorEvent.transaction],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: { filter: filters },
},
- body: {
- size: 0,
- query: {
- bool: { filter: filters },
- },
- aggs: {
- percentile: {
- percentiles: {
- field: TRANSACTION_DURATION,
- percents: [durationPercentile],
- },
+ aggs: {
+ percentile: {
+ percentiles: {
+ field: TRANSACTION_DURATION,
+ percents: [durationPercentile],
},
},
},
- });
-
- const duration = Object.values(
- res.aggregations?.percentile.values || {}
- )[0];
- return duration || 0;
+ },
});
+
+ const duration = Object.values(res.aggregations?.percentile.values || {})[0];
+ return duration || 0;
}
diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts
index 6d42b26b22e42..be1bb631378cf 100644
--- a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts
+++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts
@@ -10,7 +10,7 @@ import { ESFilter } from '../../../../../../../typings/elasticsearch';
import { ProcessorEvent } from '../../../../common/processor_event';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
import { TopSigTerm } from '../process_significant_term_aggs';
-import { withApmSpan } from '../../../utils/with_apm_span';
+
import {
getDistributionAggregation,
trimBuckets,
@@ -29,70 +29,70 @@ export async function getLatencyDistribution({
maxLatency: number;
distributionInterval: number;
}) {
- return withApmSpan('get_latency_distribution', async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const distributionAgg = getDistributionAggregation(
- maxLatency,
- distributionInterval
- );
+ const distributionAgg = getDistributionAggregation(
+ maxLatency,
+ distributionInterval
+ );
- const perTermAggs = topSigTerms.reduce(
- (acc, term, index) => {
- acc[`term_${index}`] = {
- filter: { term: { [term.fieldName]: term.fieldValue } },
- aggs: {
- distribution: distributionAgg,
- },
+ const perTermAggs = topSigTerms.reduce(
+ (acc, term, index) => {
+ acc[`term_${index}`] = {
+ filter: { term: { [term.fieldName]: term.fieldValue } },
+ aggs: {
+ distribution: distributionAgg,
+ },
+ };
+ return acc;
+ },
+ {} as Record<
+ string,
+ {
+ filter: AggregationOptionsByType['filter'];
+ aggs: {
+ distribution: typeof distributionAgg;
};
- return acc;
- },
- {} as Record<
- string,
- {
- filter: AggregationOptionsByType['filter'];
- aggs: {
- distribution: typeof distributionAgg;
- };
- }
- >
- );
+ }
+ >
+ );
- const params = {
- // TODO: add support for metrics
- apm: { events: [ProcessorEvent.transaction] },
- body: {
- size: 0,
- query: { bool: { filter: filters } },
- aggs: perTermAggs,
- },
- };
+ const params = {
+ // TODO: add support for metrics
+ apm: { events: [ProcessorEvent.transaction] },
+ body: {
+ size: 0,
+ query: { bool: { filter: filters } },
+ aggs: perTermAggs,
+ },
+ };
- const response = await withApmSpan('get_terms_distribution', () =>
- apmEventClient.search(params)
- );
- type Agg = NonNullable;
+ const response = await apmEventClient.search(
+ 'get_latency_distribution',
+ params
+ );
- if (!response.aggregations) {
- return [];
- }
+ type Agg = NonNullable;
- return topSigTerms.map((topSig, index) => {
- // ignore the typescript error since existence of response.aggregations is already checked:
- // @ts-expect-error
- const agg = response.aggregations[`term_${index}`] as Agg[string];
- const total = agg.distribution.doc_count;
- const buckets = trimBuckets(
- agg.distribution.dist_filtered_by_latency.buckets
- );
+ if (!response.aggregations) {
+ return [];
+ }
- return {
- ...topSig,
- distribution: buckets.map((bucket) => ({
- x: bucket.key,
- y: (bucket.doc_count / total) * 100,
- })),
- };
- });
+ return topSigTerms.map((topSig, index) => {
+ // ignore the typescript error since existence of response.aggregations is already checked:
+ // @ts-expect-error
+ const agg = response.aggregations[`term_${index}`] as Agg[string];
+ const total = agg.distribution.doc_count;
+ const buckets = trimBuckets(
+ agg.distribution.dist_filtered_by_latency.buckets
+ );
+
+ return {
+ ...topSig,
+ distribution: buckets.map((bucket) => ({
+ x: bucket.key,
+ y: (bucket.doc_count / total) * 100,
+ })),
+ };
});
}
diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts
index 8b415bf0d80a7..f2762086614b4 100644
--- a/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts
+++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts
@@ -8,7 +8,6 @@
import { ESFilter } from '../../../../../../../typings/elasticsearch';
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../../common/processor_event';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
import { TopSigTerm } from '../process_significant_term_aggs';
@@ -21,41 +20,39 @@ export async function getMaxLatency({
filters: ESFilter[];
topSigTerms?: TopSigTerm[];
}) {
- return withApmSpan('get_max_latency', async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const params = {
- // TODO: add support for metrics
- apm: { events: [ProcessorEvent.transaction] },
- body: {
- size: 0,
- query: {
- bool: {
- filter: filters,
+ const params = {
+ // TODO: add support for metrics
+ apm: { events: [ProcessorEvent.transaction] },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: filters,
- ...(topSigTerms.length
- ? {
- // only include docs containing the significant terms
- should: topSigTerms.map((term) => ({
- term: { [term.fieldName]: term.fieldValue },
- })),
- minimum_should_match: 1,
- }
- : null),
- },
+ ...(topSigTerms.length
+ ? {
+ // only include docs containing the significant terms
+ should: topSigTerms.map((term) => ({
+ term: { [term.fieldName]: term.fieldValue },
+ })),
+ minimum_should_match: 1,
+ }
+ : null),
},
- aggs: {
- // TODO: add support for metrics
- // max_latency: { max: { field: TRANSACTION_DURATION } },
- max_latency: {
- percentiles: { field: TRANSACTION_DURATION, percents: [99] },
- },
+ },
+ aggs: {
+ // TODO: add support for metrics
+ // max_latency: { max: { field: TRANSACTION_DURATION } },
+ max_latency: {
+ percentiles: { field: TRANSACTION_DURATION, percents: [99] },
},
},
- };
+ },
+ };
- const response = await apmEventClient.search(params);
- // return response.aggregations?.max_latency.value;
- return Object.values(response.aggregations?.max_latency.values ?? {})[0];
- });
+ const response = await apmEventClient.search('get_max_latency', params);
+ // return response.aggregations?.max_latency.value;
+ return Object.values(response.aggregations?.max_latency.values ?? {})[0];
}
diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts
index c5d4def51ea54..b0e0f22c70366 100644
--- a/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts
+++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts
@@ -71,8 +71,9 @@ export async function getOverallLatencyDistribution(
},
};
- const response = await withApmSpan('get_terms_distribution', () =>
- apmEventClient.search(params)
+ const response = await apmEventClient.search(
+ 'get_terms_distribution',
+ params
);
if (!response.aggregations) {
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 1bf01c24776fb..f6a1987974853 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
@@ -13,7 +13,6 @@ import {
} from '../../../common/elasticsearch_fieldnames';
import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions';
-import { withApmSpan } from '../../utils/with_apm_span';
/**
* This is used for getting *all* environments, and does not filter by range.
@@ -30,59 +29,56 @@ export async function getAllEnvironments({
searchAggregatedTransactions: boolean;
includeMissing?: boolean;
}) {
- const spanName = serviceName
+ const operationName = serviceName
? 'get_all_environments_for_service'
: 'get_all_environments_for_all_services';
- return withApmSpan(spanName, async () => {
- const { apmEventClient, config } = setup;
- const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
- // omit filter for service.name if "All" option is selected
- const serviceNameFilter = serviceName
- ? [{ term: { [SERVICE_NAME]: serviceName } }]
- : [];
+ const { apmEventClient, config } = setup;
+ const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
- const params = {
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ProcessorEvent.error,
- ProcessorEvent.metric,
- ],
- },
- body: {
- // use timeout + min_doc_count to return as early as possible
- // if filter is not defined to prevent timeouts
- ...(!serviceName ? { timeout: '1ms' } : {}),
- size: 0,
- query: {
- bool: {
- filter: [...serviceNameFilter],
- },
+ // omit filter for service.name if "All" option is selected
+ const serviceNameFilter = serviceName
+ ? [{ term: { [SERVICE_NAME]: serviceName } }]
+ : [];
+
+ const params = {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ProcessorEvent.error,
+ ProcessorEvent.metric,
+ ],
+ },
+ body: {
+ // use timeout + min_doc_count to return as early as possible
+ // if filter is not defined to prevent timeouts
+ ...(!serviceName ? { timeout: '1ms' } : {}),
+ size: 0,
+ query: {
+ bool: {
+ filter: [...serviceNameFilter],
},
- aggs: {
- environments: {
- terms: {
- field: SERVICE_ENVIRONMENT,
- size: maxServiceEnvironments,
- ...(!serviceName ? { min_doc_count: 0 } : {}),
- missing: includeMissing
- ? ENVIRONMENT_NOT_DEFINED.value
- : undefined,
- },
+ },
+ aggs: {
+ environments: {
+ terms: {
+ field: SERVICE_ENVIRONMENT,
+ size: maxServiceEnvironments,
+ ...(!serviceName ? { min_doc_count: 0 } : {}),
+ missing: includeMissing ? ENVIRONMENT_NOT_DEFINED.value : undefined,
},
},
},
- };
+ },
+ };
- const resp = await apmEventClient.search(params);
+ const resp = await apmEventClient.search(operationName, params);
- const environments =
- resp.aggregations?.environments.buckets.map(
- (bucket) => bucket.key as string
- ) || [];
- return environments;
- });
+ const environments =
+ resp.aggregations?.environments.buckets.map(
+ (bucket) => bucket.key as string
+ ) || [];
+ return environments;
}
diff --git a/x-pack/plugins/apm/server/lib/environments/get_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.ts
index 509e4cdcd67ac..c0b267f180010 100644
--- a/x-pack/plugins/apm/server/lib/environments/get_environments.ts
+++ b/x-pack/plugins/apm/server/lib/environments/get_environments.ts
@@ -12,7 +12,6 @@ import {
import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
import { ProcessorEvent } from '../../../common/processor_event';
import { rangeQuery } from '../../../server/utils/queries';
-import { withApmSpan } from '../../utils/with_apm_span';
import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
@@ -29,60 +28,58 @@ export async function getEnvironments({
serviceName?: string;
searchAggregatedTransactions: boolean;
}) {
- const spanName = serviceName
+ const operationName = serviceName
? 'get_environments_for_service'
: 'get_environments';
- return withApmSpan(spanName, async () => {
- const { start, end, apmEventClient, config } = setup;
+ const { start, end, apmEventClient, config } = setup;
- const filter = rangeQuery(start, end);
+ const filter = rangeQuery(start, end);
- if (serviceName) {
- filter.push({
- term: { [SERVICE_NAME]: serviceName },
- });
- }
+ if (serviceName) {
+ filter.push({
+ term: { [SERVICE_NAME]: serviceName },
+ });
+ }
- const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
+ const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
- const params = {
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ProcessorEvent.metric,
- ProcessorEvent.error,
- ],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter,
- },
+ const params = {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ProcessorEvent.metric,
+ ProcessorEvent.error,
+ ],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter,
},
- aggs: {
- environments: {
- terms: {
- field: SERVICE_ENVIRONMENT,
- missing: ENVIRONMENT_NOT_DEFINED.value,
- size: maxServiceEnvironments,
- },
+ },
+ aggs: {
+ environments: {
+ terms: {
+ field: SERVICE_ENVIRONMENT,
+ missing: ENVIRONMENT_NOT_DEFINED.value,
+ size: maxServiceEnvironments,
},
},
},
- };
+ },
+ };
- const resp = await apmEventClient.search(params);
- const aggs = resp.aggregations;
- const environmentsBuckets = aggs?.environments.buckets || [];
+ const resp = await apmEventClient.search(operationName, params);
+ const aggs = resp.aggregations;
+ const environmentsBuckets = aggs?.environments.buckets || [];
- const environments = environmentsBuckets.map(
- (environmentBucket) => environmentBucket.key as string
- );
+ const environments = environmentsBuckets.map(
+ (environmentBucket) => environmentBucket.key as string
+ );
- return environments;
- });
+ return environments;
}
diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap
index 43fe4dfe752e6..2c0330f17320d 100644
--- a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap
@@ -3,6 +3,7 @@
exports[`get buckets should make the correct query 1`] = `
Array [
Array [
+ "get_error_distribution_buckets",
Object {
"apm": Object {
"events": Array [
diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts
index b1260d653f3de..712343d445d44 100644
--- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts
+++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts
@@ -65,7 +65,7 @@ describe('get buckets', () => {
});
it('should limit query results to error documents', () => {
- const query = clientSpy.mock.calls[0][0];
+ const query = clientSpy.mock.calls[0][1];
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 462c9bcdc4310..a51464764f2b4 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
@@ -16,7 +16,6 @@ import {
rangeQuery,
kqlQuery,
} from '../../../../server/utils/queries';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
export async function getBuckets({
@@ -34,58 +33,59 @@ export async function getBuckets({
bucketSize: number;
setup: Setup & SetupTimeRange;
}) {
- return withApmSpan('get_error_distribution_buckets', async () => {
- const { start, end, apmEventClient } = setup;
- const filter: ESFilter[] = [
- { term: { [SERVICE_NAME]: serviceName } },
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ...kqlQuery(kuery),
- ];
+ const { start, end, apmEventClient } = setup;
+ const filter: ESFilter[] = [
+ { term: { [SERVICE_NAME]: serviceName } },
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ ];
- if (groupId) {
- filter.push({ term: { [ERROR_GROUP_ID]: groupId } });
- }
+ if (groupId) {
+ filter.push({ term: { [ERROR_GROUP_ID]: groupId } });
+ }
- const params = {
- apm: {
- events: [ProcessorEvent.error],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter,
- },
+ const params = {
+ apm: {
+ events: [ProcessorEvent.error],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter,
},
- aggs: {
- distribution: {
- histogram: {
- field: '@timestamp',
- min_doc_count: 0,
- interval: bucketSize,
- extended_bounds: {
- min: start,
- max: end,
- },
+ },
+ aggs: {
+ distribution: {
+ histogram: {
+ field: '@timestamp',
+ min_doc_count: 0,
+ interval: bucketSize,
+ extended_bounds: {
+ min: start,
+ max: end,
},
},
},
},
- };
+ },
+ };
- const resp = await apmEventClient.search(params);
+ const resp = await apmEventClient.search(
+ 'get_error_distribution_buckets',
+ params
+ );
- const buckets = (resp.aggregations?.distribution.buckets || []).map(
- (bucket) => ({
- key: bucket.key,
- count: bucket.doc_count,
- })
- );
+ const buckets = (resp.aggregations?.distribution.buckets || []).map(
+ (bucket) => ({
+ key: bucket.key,
+ count: bucket.doc_count,
+ })
+ );
- return {
- noHits: resp.hits.total.value === 0,
- buckets: resp.hits.total.value > 0 ? buckets : [],
- };
- });
+ return {
+ noHits: resp.hits.total.value === 0,
+ buckets: resp.hits.total.value > 0 ? buckets : [],
+ };
}
diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts
index 57fb486180993..a915a4fb03305 100644
--- a/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts
+++ b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts
@@ -17,11 +17,10 @@ import {
rangeQuery,
kqlQuery,
} from '../../../server/utils/queries';
-import { withApmSpan } from '../../utils/with_apm_span';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { getTransaction } from '../transactions/get_transaction';
-export function getErrorGroupSample({
+export async function getErrorGroupSample({
environment,
kuery,
serviceName,
@@ -34,48 +33,46 @@ export function getErrorGroupSample({
groupId: string;
setup: Setup & SetupTimeRange;
}) {
- return withApmSpan('get_error_group_sample', async () => {
- const { start, end, apmEventClient } = setup;
+ const { start, end, apmEventClient } = setup;
- const params = {
- apm: {
- events: [ProcessorEvent.error as const],
- },
- body: {
- size: 1,
- query: {
- bool: {
- filter: [
- { term: { [SERVICE_NAME]: serviceName } },
- { term: { [ERROR_GROUP_ID]: groupId } },
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ...kqlQuery(kuery),
- ],
- should: [{ term: { [TRANSACTION_SAMPLED]: true } }],
- },
+ const params = {
+ apm: {
+ events: [ProcessorEvent.error as const],
+ },
+ body: {
+ size: 1,
+ query: {
+ bool: {
+ filter: [
+ { term: { [SERVICE_NAME]: serviceName } },
+ { term: { [ERROR_GROUP_ID]: groupId } },
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ ],
+ should: [{ term: { [TRANSACTION_SAMPLED]: true } }],
},
- sort: asMutableArray([
- { _score: 'desc' }, // sort by _score first to ensure that errors with transaction.sampled:true ends up on top
- { '@timestamp': { order: 'desc' } }, // sort by timestamp to get the most recent error
- ] as const),
},
- };
+ sort: asMutableArray([
+ { _score: 'desc' }, // sort by _score first to ensure that errors with transaction.sampled:true ends up on top
+ { '@timestamp': { order: 'desc' } }, // sort by timestamp to get the most recent error
+ ] as const),
+ },
+ };
- const resp = await apmEventClient.search(params);
- const error = resp.hits.hits[0]?._source;
- const transactionId = error?.transaction?.id;
- const traceId = error?.trace?.id;
+ const resp = await apmEventClient.search('get_error_group_sample', params);
+ const error = resp.hits.hits[0]?._source;
+ const transactionId = error?.transaction?.id;
+ const traceId = error?.trace?.id;
- let transaction;
- if (transactionId && traceId) {
- transaction = await getTransaction({ transactionId, traceId, setup });
- }
+ let transaction;
+ if (transactionId && traceId) {
+ transaction = await getTransaction({ transactionId, traceId, setup });
+ }
- return {
- transaction,
- error,
- occurrencesCount: resp.hits.total.value,
- };
- });
+ return {
+ transaction,
+ error,
+ occurrencesCount: resp.hits.total.value,
+ };
}
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 f5b22e5349756..d705a2eb5a00c 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
@@ -15,11 +15,10 @@ import {
} from '../../../common/elasticsearch_fieldnames';
import { getErrorGroupsProjection } from '../../projections/errors';
import { mergeProjection } from '../../projections/util/merge_projection';
-import { withApmSpan } from '../../utils/with_apm_span';
import { getErrorName } from '../helpers/get_error_name';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
-export function getErrorGroups({
+export async function getErrorGroups({
environment,
kuery,
serviceName,
@@ -34,87 +33,83 @@ export function getErrorGroups({
sortDirection?: 'asc' | 'desc';
setup: Setup & SetupTimeRange;
}) {
- return withApmSpan('get_error_groups', async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- // sort buckets by last occurrence of error
- const sortByLatestOccurrence = sortField === 'latestOccurrenceAt';
+ // sort buckets by last occurrence of error
+ const sortByLatestOccurrence = sortField === 'latestOccurrenceAt';
- const projection = getErrorGroupsProjection({
- environment,
- kuery,
- setup,
- serviceName,
- });
+ const projection = getErrorGroupsProjection({
+ environment,
+ kuery,
+ setup,
+ serviceName,
+ });
- const order = sortByLatestOccurrence
- ? {
- max_timestamp: sortDirection,
- }
- : { _count: sortDirection };
+ const order = sortByLatestOccurrence
+ ? {
+ max_timestamp: sortDirection,
+ }
+ : { _count: sortDirection };
- const params = mergeProjection(projection, {
- body: {
- size: 0,
- aggs: {
- error_groups: {
- terms: {
- ...projection.body.aggs.error_groups.terms,
- size: 500,
- order,
- },
- aggs: {
- sample: {
- top_hits: {
- _source: [
- ERROR_LOG_MESSAGE,
- ERROR_EXC_MESSAGE,
- ERROR_EXC_HANDLED,
- ERROR_EXC_TYPE,
- ERROR_CULPRIT,
- ERROR_GROUP_ID,
- '@timestamp',
- ],
- sort: [{ '@timestamp': 'desc' as const }],
- size: 1,
- },
+ const params = mergeProjection(projection, {
+ body: {
+ size: 0,
+ aggs: {
+ error_groups: {
+ terms: {
+ ...projection.body.aggs.error_groups.terms,
+ size: 500,
+ order,
+ },
+ aggs: {
+ sample: {
+ top_hits: {
+ _source: [
+ ERROR_LOG_MESSAGE,
+ ERROR_EXC_MESSAGE,
+ ERROR_EXC_HANDLED,
+ ERROR_EXC_TYPE,
+ ERROR_CULPRIT,
+ ERROR_GROUP_ID,
+ '@timestamp',
+ ],
+ sort: [{ '@timestamp': 'desc' as const }],
+ size: 1,
},
- ...(sortByLatestOccurrence
- ? {
- max_timestamp: {
- max: {
- field: '@timestamp',
- },
- },
- }
- : {}),
},
+ ...(sortByLatestOccurrence
+ ? {
+ max_timestamp: {
+ max: {
+ field: '@timestamp',
+ },
+ },
+ }
+ : {}),
},
},
},
- });
+ },
+ });
- const resp = await apmEventClient.search(params);
+ const resp = await apmEventClient.search('get_error_groups', 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.
- const hits = (resp.aggregations?.error_groups.buckets || []).map(
- (bucket) => {
- const source = bucket.sample.hits.hits[0]._source;
- const message = getErrorName(source);
+ // 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.
+ const hits = (resp.aggregations?.error_groups.buckets || []).map((bucket) => {
+ const source = bucket.sample.hits.hits[0]._source;
+ const message = getErrorName(source);
- return {
- message,
- occurrenceCount: bucket.doc_count,
- culprit: source.error.culprit,
- groupId: source.error.grouping_key,
- latestOccurrenceAt: source['@timestamp'],
- handled: source.error.exception?.[0].handled,
- type: source.error.exception?.[0].type,
- };
- }
- );
-
- return hits;
+ return {
+ message,
+ occurrenceCount: bucket.doc_count,
+ culprit: source.error.culprit,
+ groupId: source.error.grouping_key,
+ latestOccurrenceAt: source['@timestamp'],
+ handled: source.error.exception?.[0].handled,
+ type: source.error.exception?.[0].type,
+ };
});
+
+ return hits;
}
diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts
index 394cf6b988f12..8bfb137c1689c 100644
--- a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts
@@ -14,7 +14,6 @@ import {
} from '../../../../common/elasticsearch_fieldnames';
import { APMConfig } from '../../..';
import { APMEventClient } from '../create_es_client/create_apm_event_client';
-import { withApmSpan } from '../../../utils/with_apm_span';
export async function getHasAggregatedTransactions({
start,
@@ -25,8 +24,9 @@ export async function getHasAggregatedTransactions({
end?: number;
apmEventClient: APMEventClient;
}) {
- return withApmSpan('get_has_aggregated_transactions', async () => {
- const response = await apmEventClient.search({
+ const response = await apmEventClient.search(
+ 'get_has_aggregated_transactions',
+ {
apm: {
events: [ProcessorEvent.metric],
},
@@ -41,14 +41,14 @@ export async function getHasAggregatedTransactions({
},
},
terminateAfter: 1,
- });
-
- if (response.hits.total.value > 0) {
- return true;
}
+ );
+
+ if (response.hits.total.value > 0) {
+ return true;
+ }
- return false;
- });
+ return false;
}
export async function getSearchAggregatedTransactions({
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts
index 989297544c78f..39018e26f371c 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts
@@ -81,17 +81,26 @@ export async function callAsyncWithDebug({
return res;
}
-export const getDebugBody = (
- params: Record,
- requestType: string
-) => {
+export const getDebugBody = ({
+ params,
+ requestType,
+ operationName,
+}: {
+ params: Record;
+ requestType: string;
+ operationName: string;
+}) => {
+ const operationLine = `${operationName}\n`;
+
if (requestType === 'search') {
- return `GET ${params.index}/_search\n${formatObj(params.body)}`;
+ return `${operationLine}GET ${params.index}/_search\n${formatObj(
+ params.body
+ )}`;
}
return `${chalk.bold('ES operation:')} ${requestType}\n${chalk.bold(
'ES query:'
- )}\n${formatObj(params)}`;
+ )}\n${operationLine}${formatObj(params)}`;
};
export const getDebugTitle = (request: KibanaRequest) =>
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts
index addd7391d782d..8e82a189d75f3 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts
@@ -47,7 +47,7 @@ describe('createApmEventClient', () => {
},
});
- await eventClient.search({
+ await eventClient.search('foo', {
apm: {
events: [],
},
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
index b8a14253a229a..916a6981f286a 100644
--- 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
@@ -6,6 +6,7 @@
*/
import { ValuesType } from 'utility-types';
+import { withApmSpan } from '../../../../utils/with_apm_span';
import { Profile } from '../../../../../typings/es_schemas/ui/profile';
import {
ElasticsearchClient,
@@ -34,6 +35,7 @@ import { unpackProcessorEvents } from './unpack_processor_events';
export type APMEventESSearchRequest = Omit & {
apm: {
events: ProcessorEvent[];
+ includeLegacyData?: boolean;
};
};
@@ -78,11 +80,13 @@ export function createApmEventClient({
}) {
return {
async search(
- params: TParams,
- { includeLegacyData = false } = {}
+ operationName: string,
+ params: TParams
): Promise> {
const withProcessorEventFilter = unpackProcessorEvents(params, indices);
+ const { includeLegacyData = false } = params.apm;
+
const withPossibleLegacyDataFilter = !includeLegacyData
? addFilterToExcludeLegacyData(withProcessorEventFilter)
: withProcessorEventFilter;
@@ -98,15 +102,18 @@ export function createApmEventClient({
return callAsyncWithDebug({
cb: () => {
- const searchPromise = cancelEsRequestOnAbort(
- esClient.search(searchParams),
- request
+ const searchPromise = withApmSpan(operationName, () =>
+ cancelEsRequestOnAbort(esClient.search(searchParams), request)
);
return unwrapEsResponse(searchPromise);
},
getDebugMessage: () => ({
- body: getDebugBody(searchParams, requestType),
+ body: getDebugBody({
+ params: searchParams,
+ requestType,
+ operationName,
+ }),
title: getDebugTitle(request),
}),
isCalledWithInternalUser: false,
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
index 1544538de74a6..e6b61a709ae35 100644
--- 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
@@ -31,20 +31,23 @@ export function createInternalESClient({
}: Pick & { debug: boolean }) {
const { asInternalUser } = context.core.elasticsearch.client;
- function callEs({
- cb,
- requestType,
- params,
- }: {
- requestType: string;
- cb: () => TransportRequestPromise;
- params: Record;
- }) {
+ function callEs(
+ operationName: string,
+ {
+ cb,
+ requestType,
+ params,
+ }: {
+ requestType: string;
+ cb: () => TransportRequestPromise;
+ params: Record;
+ }
+ ) {
return callAsyncWithDebug({
cb: () => unwrapEsResponse(cancelEsRequestOnAbort(cb(), request)),
getDebugMessage: () => ({
title: getDebugTitle(request),
- body: getDebugBody(params, requestType),
+ body: getDebugBody({ params, requestType, operationName }),
}),
debug,
isCalledWithInternalUser: true,
@@ -59,30 +62,37 @@ export function createInternalESClient({
TDocument = unknown,
TSearchRequest extends ESSearchRequest = ESSearchRequest
>(
+ operationName: string,
params: TSearchRequest
): Promise> => {
- return callEs({
+ return callEs(operationName, {
requestType: 'search',
cb: () => asInternalUser.search(params),
params,
});
},
- index: (params: APMIndexDocumentParams) => {
- return callEs({
+ index: (operationName: string, params: APMIndexDocumentParams) => {
+ return callEs(operationName, {
requestType: 'index',
cb: () => asInternalUser.index(params),
params,
});
},
- delete: (params: estypes.DeleteRequest): Promise<{ result: string }> => {
- return callEs({
+ delete: (
+ operationName: string,
+ params: estypes.DeleteRequest
+ ): Promise<{ result: string }> => {
+ return callEs(operationName, {
requestType: 'delete',
cb: () => asInternalUser.delete(params),
params,
});
},
- indicesCreate: (params: estypes.IndicesCreateRequest) => {
- return callEs({
+ indicesCreate: (
+ operationName: string,
+ params: estypes.IndicesCreateRequest
+ ) => {
+ return callEs(operationName, {
requestType: 'indices.create',
cb: () => asInternalUser.indices.create(params),
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 c0ff0cab88f47..66b3c91fc6f2d 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
@@ -109,7 +109,7 @@ describe('setupRequest', () => {
it('calls callWithRequest', async () => {
const mockResources = getMockResources();
const { apmEventClient } = await setupRequest(mockResources);
- await apmEventClient.search({
+ await apmEventClient.search('foo', {
apm: { events: [ProcessorEvent.transaction] },
body: { foo: 'bar' },
});
@@ -137,7 +137,7 @@ describe('setupRequest', () => {
it('calls callWithInternalUser', async () => {
const mockResources = getMockResources();
const { internalClient } = await setupRequest(mockResources);
- await internalClient.search({
+ await internalClient.search('foo', {
index: ['apm-*'],
body: { foo: 'bar' },
} as any);
@@ -156,7 +156,7 @@ describe('setupRequest', () => {
it('adds a range filter for `observer.version_major` to the existing filter', async () => {
const mockResources = getMockResources();
const { apmEventClient } = await setupRequest(mockResources);
- await apmEventClient.search({
+ await apmEventClient.search('foo', {
apm: {
events: [ProcessorEvent.transaction],
},
@@ -183,19 +183,15 @@ describe('setupRequest', () => {
it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => {
const mockResources = getMockResources();
const { apmEventClient } = await setupRequest(mockResources);
- await apmEventClient.search(
- {
- apm: {
- events: [ProcessorEvent.error],
- },
- body: {
- query: { bool: { filter: [{ term: { field: 'someTerm' } }] } },
- },
- },
- {
+ await apmEventClient.search('foo', {
+ apm: {
+ events: [ProcessorEvent.error],
includeLegacyData: true,
- }
- );
+ },
+ body: {
+ query: { bool: { filter: [{ term: { field: 'someTerm' } }] } },
+ },
+ });
const params =
mockResources.context.core.elasticsearch.client.asCurrentUser.search
.mock.calls[0][0];
@@ -221,7 +217,7 @@ describe('without a bool filter', () => {
it('adds a range filter for `observer.version_major`', async () => {
const mockResources = getMockResources();
const { apmEventClient } = await setupRequest(mockResources);
- await apmEventClient.search({
+ await apmEventClient.search('foo', {
apm: {
events: [ProcessorEvent.error],
},
@@ -251,7 +247,7 @@ describe('with includeFrozen=false', () => {
const { apmEventClient } = await setupRequest(mockResources);
- await apmEventClient.search({
+ await apmEventClient.search('foo', {
apm: {
events: [],
},
@@ -273,7 +269,7 @@ describe('with includeFrozen=true', () => {
const { apmEventClient } = await setupRequest(mockResources);
- await apmEventClient.search({
+ await apmEventClient.search('foo', {
apm: { events: [] },
});
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 3b3ef8b9c4bcf..d1040b49dcd8b 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
@@ -30,6 +30,7 @@ export async function fetchAndTransformGcMetrics({
serviceNodeName,
chartBase,
fieldName,
+ operationName,
}: {
environment?: string;
kuery?: string;
@@ -38,6 +39,7 @@ export async function fetchAndTransformGcMetrics({
serviceNodeName?: string;
chartBase: ChartBase;
fieldName: typeof METRIC_JAVA_GC_COUNT | typeof METRIC_JAVA_GC_TIME;
+ operationName: string;
}) {
const { start, end, apmEventClient, config } = setup;
@@ -108,7 +110,7 @@ export async function fetchAndTransformGcMetrics({
},
});
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search(operationName, params);
const { aggregations } = response;
diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts
index 388331f3bbf17..3ec40d5171694 100644
--- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts
@@ -7,7 +7,6 @@
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
-import { withApmSpan } from '../../../../../utils/with_apm_span';
import { METRIC_JAVA_GC_COUNT } from '../../../../../../common/elasticsearch_fieldnames';
import { Setup, SetupTimeRange } from '../../../../helpers/setup_request';
import { fetchAndTransformGcMetrics } from './fetch_and_transform_gc_metrics';
@@ -45,17 +44,16 @@ function getGcRateChart({
serviceName: string;
serviceNodeName?: string;
}) {
- return withApmSpan('get_gc_rate_charts', () =>
- fetchAndTransformGcMetrics({
- environment,
- kuery,
- setup,
- serviceName,
- serviceNodeName,
- chartBase,
- fieldName: METRIC_JAVA_GC_COUNT,
- })
- );
+ return fetchAndTransformGcMetrics({
+ environment,
+ kuery,
+ setup,
+ serviceName,
+ serviceNodeName,
+ chartBase,
+ fieldName: METRIC_JAVA_GC_COUNT,
+ operationName: 'get_gc_rate_charts',
+ });
}
export { getGcRateChart };
diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts
index e6f80190d1daa..8e4416d94fb90 100644
--- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts
@@ -7,7 +7,6 @@
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
-import { withApmSpan } from '../../../../../utils/with_apm_span';
import { METRIC_JAVA_GC_TIME } from '../../../../../../common/elasticsearch_fieldnames';
import { Setup, SetupTimeRange } from '../../../../helpers/setup_request';
import { fetchAndTransformGcMetrics } from './fetch_and_transform_gc_metrics';
@@ -45,17 +44,16 @@ function getGcTimeChart({
serviceName: string;
serviceNodeName?: string;
}) {
- return withApmSpan('get_gc_time_charts', () =>
- fetchAndTransformGcMetrics({
- environment,
- kuery,
- setup,
- serviceName,
- serviceNodeName,
- chartBase,
- fieldName: METRIC_JAVA_GC_TIME,
- })
- );
+ return fetchAndTransformGcMetrics({
+ environment,
+ kuery,
+ setup,
+ serviceName,
+ serviceNodeName,
+ chartBase,
+ fieldName: METRIC_JAVA_GC_TIME,
+ operationName: 'get_gc_time_charts',
+ });
}
export { getGcTimeChart };
diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts
index 7630827a3cb38..6a23213e94537 100644
--- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts
@@ -7,7 +7,6 @@
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
-import { withApmSpan } from '../../../../../utils/with_apm_span';
import {
METRIC_JAVA_HEAP_MEMORY_MAX,
METRIC_JAVA_HEAP_MEMORY_COMMITTED,
@@ -65,22 +64,21 @@ export function getHeapMemoryChart({
serviceName: string;
serviceNodeName?: string;
}) {
- return withApmSpan('get_heap_memory_charts', () =>
- fetchAndTransformMetrics({
- environment,
- kuery,
- setup,
- serviceName,
- serviceNodeName,
- chartBase,
- aggs: {
- heapMemoryMax: { avg: { field: METRIC_JAVA_HEAP_MEMORY_MAX } },
- heapMemoryCommitted: {
- avg: { field: METRIC_JAVA_HEAP_MEMORY_COMMITTED },
- },
- heapMemoryUsed: { avg: { field: METRIC_JAVA_HEAP_MEMORY_USED } },
+ return fetchAndTransformMetrics({
+ environment,
+ kuery,
+ setup,
+ serviceName,
+ serviceNodeName,
+ chartBase,
+ aggs: {
+ heapMemoryMax: { avg: { field: METRIC_JAVA_HEAP_MEMORY_MAX } },
+ heapMemoryCommitted: {
+ avg: { field: METRIC_JAVA_HEAP_MEMORY_COMMITTED },
},
- additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }],
- })
- );
+ heapMemoryUsed: { avg: { field: METRIC_JAVA_HEAP_MEMORY_USED } },
+ },
+ additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }],
+ operationName: 'get_heap_memory_charts',
+ });
}
diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts
index cd11e5e5383b6..1ceb42b7db479 100644
--- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts
@@ -7,7 +7,6 @@
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
-import { withApmSpan } from '../../../../../utils/with_apm_span';
import {
METRIC_JAVA_NON_HEAP_MEMORY_MAX,
METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED,
@@ -62,24 +61,23 @@ export async function getNonHeapMemoryChart({
serviceName: string;
serviceNodeName?: string;
}) {
- return withApmSpan('get_non_heap_memory_charts', () =>
- fetchAndTransformMetrics({
- environment,
- kuery,
- setup,
- serviceName,
- serviceNodeName,
- chartBase,
- aggs: {
- nonHeapMemoryMax: { avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_MAX } },
- nonHeapMemoryCommitted: {
- avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED },
- },
- nonHeapMemoryUsed: {
- avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_USED },
- },
+ return fetchAndTransformMetrics({
+ environment,
+ kuery,
+ setup,
+ serviceName,
+ serviceNodeName,
+ chartBase,
+ aggs: {
+ nonHeapMemoryMax: { avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_MAX } },
+ nonHeapMemoryCommitted: {
+ avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED },
},
- additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }],
- })
- );
+ nonHeapMemoryUsed: {
+ avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_USED },
+ },
+ },
+ additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }],
+ operationName: 'get_non_heap_memory_charts',
+ });
}
diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts
index 8d4c079197d19..700c5e08d4def 100644
--- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts
@@ -7,7 +7,6 @@
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
-import { withApmSpan } from '../../../../../utils/with_apm_span';
import {
METRIC_JAVA_THREAD_COUNT,
AGENT_NAME,
@@ -54,19 +53,18 @@ export async function getThreadCountChart({
serviceName: string;
serviceNodeName?: string;
}) {
- return withApmSpan('get_thread_count_charts', () =>
- fetchAndTransformMetrics({
- environment,
- kuery,
- setup,
- serviceName,
- serviceNodeName,
- chartBase,
- aggs: {
- threadCount: { avg: { field: METRIC_JAVA_THREAD_COUNT } },
- threadCountMax: { max: { field: METRIC_JAVA_THREAD_COUNT } },
- },
- additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }],
- })
- );
+ return fetchAndTransformMetrics({
+ environment,
+ kuery,
+ setup,
+ serviceName,
+ serviceNodeName,
+ chartBase,
+ aggs: {
+ threadCount: { avg: { field: METRIC_JAVA_THREAD_COUNT } },
+ threadCountMax: { max: { field: METRIC_JAVA_THREAD_COUNT } },
+ },
+ additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }],
+ operationName: 'get_thread_count_charts',
+ });
}
diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts
index 37bef191ae876..a568d58bdd438 100644
--- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts
@@ -7,7 +7,6 @@
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
-import { withApmSpan } from '../../../../../utils/with_apm_span';
import {
METRIC_SYSTEM_CPU_PERCENT,
METRIC_PROCESS_CPU_PERCENT,
@@ -66,20 +65,19 @@ export function getCPUChartData({
serviceName: string;
serviceNodeName?: string;
}) {
- return withApmSpan('get_cpu_metric_charts', () =>
- fetchAndTransformMetrics({
- environment,
- kuery,
- setup,
- serviceName,
- serviceNodeName,
- chartBase,
- aggs: {
- systemCPUAverage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } },
- systemCPUMax: { max: { field: METRIC_SYSTEM_CPU_PERCENT } },
- processCPUAverage: { avg: { field: METRIC_PROCESS_CPU_PERCENT } },
- processCPUMax: { max: { field: METRIC_PROCESS_CPU_PERCENT } },
- },
- })
- );
+ return fetchAndTransformMetrics({
+ environment,
+ kuery,
+ setup,
+ serviceName,
+ serviceNodeName,
+ chartBase,
+ aggs: {
+ systemCPUAverage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } },
+ systemCPUMax: { max: { field: METRIC_SYSTEM_CPU_PERCENT } },
+ processCPUAverage: { avg: { field: METRIC_PROCESS_CPU_PERCENT } },
+ processCPUMax: { max: { field: METRIC_PROCESS_CPU_PERCENT } },
+ },
+ operationName: 'get_cpu_metric_charts',
+ });
}
diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts
index 0ec2f2c2fcfb2..1f7860d567b03 100644
--- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts
@@ -84,45 +84,41 @@ export async function getMemoryChartData({
serviceNodeName?: string;
}) {
return withApmSpan('get_memory_metrics_charts', async () => {
- const cgroupResponse = await withApmSpan(
- 'get_cgroup_memory_metrics_charts',
- () =>
- fetchAndTransformMetrics({
- environment,
- kuery,
- setup,
- serviceName,
- serviceNodeName,
- chartBase,
- aggs: {
- memoryUsedAvg: { avg: { script: percentCgroupMemoryUsedScript } },
- memoryUsedMax: { max: { script: percentCgroupMemoryUsedScript } },
- },
- additionalFilters: [
- { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } },
- ],
- })
- );
+ const cgroupResponse = await fetchAndTransformMetrics({
+ environment,
+ kuery,
+ setup,
+ serviceName,
+ serviceNodeName,
+ chartBase,
+ aggs: {
+ memoryUsedAvg: { avg: { script: percentCgroupMemoryUsedScript } },
+ memoryUsedMax: { max: { script: percentCgroupMemoryUsedScript } },
+ },
+ additionalFilters: [
+ { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } },
+ ],
+ operationName: 'get_cgroup_memory_metrics_charts',
+ });
if (cgroupResponse.noHits) {
- return await withApmSpan('get_system_memory_metrics_charts', () =>
- fetchAndTransformMetrics({
- environment,
- kuery,
- setup,
- serviceName,
- serviceNodeName,
- chartBase,
- aggs: {
- memoryUsedAvg: { avg: { script: percentSystemMemoryUsedScript } },
- memoryUsedMax: { max: { script: percentSystemMemoryUsedScript } },
- },
- additionalFilters: [
- { exists: { field: METRIC_SYSTEM_FREE_MEMORY } },
- { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
- ],
- })
- );
+ return await fetchAndTransformMetrics({
+ environment,
+ kuery,
+ setup,
+ serviceName,
+ serviceNodeName,
+ chartBase,
+ aggs: {
+ memoryUsedAvg: { avg: { script: percentSystemMemoryUsedScript } },
+ memoryUsedMax: { max: { script: percentSystemMemoryUsedScript } },
+ },
+ additionalFilters: [
+ { exists: { field: METRIC_SYSTEM_FREE_MEMORY } },
+ { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
+ ],
+ operationName: 'get_system_memory_metrics_charts',
+ });
}
return cgroupResponse;
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 30234447821ec..df9e33e6f4b40 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
@@ -56,6 +56,7 @@ export async function fetchAndTransformMetrics({
chartBase,
aggs,
additionalFilters = [],
+ operationName,
}: {
environment?: string;
kuery?: string;
@@ -65,6 +66,7 @@ export async function fetchAndTransformMetrics({
chartBase: ChartBase;
aggs: T;
additionalFilters?: Filter[];
+ operationName: string;
}) {
const { start, end, apmEventClient, config } = setup;
@@ -98,7 +100,7 @@ export async function fetchAndTransformMetrics({
},
});
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search(operationName, 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 2ccbe318862f1..086516371387e 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
@@ -10,40 +10,40 @@ import { rangeQuery } from '../../../server/utils/queries';
import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions';
-import { withApmSpan } from '../../utils/with_apm_span';
-export function getServiceCount({
+export async function getServiceCount({
setup,
searchAggregatedTransactions,
}: {
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
}) {
- return withApmSpan('observability_overview_get_service_count', async () => {
- const { apmEventClient, start, end } = setup;
+ const { apmEventClient, start, end } = setup;
- const params = {
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ProcessorEvent.error,
- ProcessorEvent.metric,
- ],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter: rangeQuery(start, end),
- },
+ const params = {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ProcessorEvent.error,
+ ProcessorEvent.metric,
+ ],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: rangeQuery(start, end),
},
- aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } },
},
- };
+ aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } },
+ },
+ };
- const { aggregations } = await apmEventClient.search(params);
- return aggregations?.serviceCount.value || 0;
- });
+ const { aggregations } = await apmEventClient.search(
+ 'observability_overview_get_service_count',
+ params
+ );
+ return aggregations?.serviceCount.value || 0;
}
diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts
index da8ac7c50b594..016cb50566da0 100644
--- a/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts
+++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts
@@ -14,9 +14,8 @@ import { rangeQuery } from '../../../server/utils/queries';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions';
import { calculateThroughput } from '../helpers/calculate_throughput';
-import { withApmSpan } from '../../utils/with_apm_span';
-export function getTransactionsPerMinute({
+export async function getTransactionsPerMinute({
setup,
bucketSize,
searchAggregatedTransactions,
@@ -25,71 +24,69 @@ export function getTransactionsPerMinute({
bucketSize: string;
searchAggregatedTransactions: boolean;
}) {
- return withApmSpan(
- 'observability_overview_get_transactions_per_minute',
- async () => {
- const { apmEventClient, start, end } = setup;
+ const { apmEventClient, start, end } = setup;
- const { aggregations } = await apmEventClient.search({
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ],
+ const { aggregations } = await apmEventClient.search(
+ 'observability_overview_get_transactions_per_minute',
+ {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: rangeQuery(start, end),
+ },
},
- body: {
- size: 0,
- query: {
- bool: {
- filter: rangeQuery(start, end),
+ aggs: {
+ transactionType: {
+ terms: {
+ field: TRANSACTION_TYPE,
},
- },
- aggs: {
- transactionType: {
- terms: {
- field: TRANSACTION_TYPE,
- },
- aggs: {
- timeseries: {
- date_histogram: {
- field: '@timestamp',
- fixed_interval: bucketSize,
- min_doc_count: 0,
- },
- aggs: {
- throughput: { rate: { unit: 'minute' as const } },
- },
+ aggs: {
+ timeseries: {
+ date_histogram: {
+ field: '@timestamp',
+ fixed_interval: bucketSize,
+ min_doc_count: 0,
+ },
+ aggs: {
+ throughput: { rate: { unit: 'minute' as const } },
},
},
},
},
},
- });
+ },
+ }
+ );
- if (!aggregations || !aggregations.transactionType.buckets) {
- return { value: undefined, timeseries: [] };
- }
+ if (!aggregations || !aggregations.transactionType.buckets) {
+ return { value: undefined, timeseries: [] };
+ }
- const topTransactionTypeBucket =
- aggregations.transactionType.buckets.find(
- ({ key: transactionType }) =>
- transactionType === TRANSACTION_REQUEST ||
- transactionType === TRANSACTION_PAGE_LOAD
- ) || aggregations.transactionType.buckets[0];
+ const topTransactionTypeBucket =
+ aggregations.transactionType.buckets.find(
+ ({ key: transactionType }) =>
+ transactionType === TRANSACTION_REQUEST ||
+ transactionType === TRANSACTION_PAGE_LOAD
+ ) || aggregations.transactionType.buckets[0];
- return {
- value: calculateThroughput({
- start,
- end,
- value: topTransactionTypeBucket?.doc_count || 0,
- }),
- timeseries:
- topTransactionTypeBucket?.timeseries.buckets.map((bucket) => ({
- x: bucket.key,
- y: bucket.throughput.value,
- })) || [],
- };
- }
- );
+ return {
+ value: calculateThroughput({
+ start,
+ end,
+ value: topTransactionTypeBucket?.doc_count || 0,
+ }),
+ timeseries:
+ topTransactionTypeBucket?.timeseries.buckets.map((bucket) => ({
+ x: bucket.key,
+ y: bucket.throughput.value,
+ })) || [],
+ };
}
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 bbe13874d7d3b..5c1a33e750e12 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
@@ -6,31 +6,31 @@
*/
import { ProcessorEvent } from '../../../common/processor_event';
-import { withApmSpan } from '../../utils/with_apm_span';
import { Setup } from '../helpers/setup_request';
-export function getHasData({ setup }: { setup: Setup }) {
- return withApmSpan('observability_overview_has_apm_data', async () => {
- const { apmEventClient } = setup;
- try {
- const params = {
- apm: {
- events: [
- ProcessorEvent.transaction,
- ProcessorEvent.error,
- ProcessorEvent.metric,
- ],
- },
- terminateAfter: 1,
- body: {
- size: 0,
- },
- };
+export async function getHasData({ setup }: { setup: Setup }) {
+ const { apmEventClient } = setup;
+ try {
+ const params = {
+ apm: {
+ events: [
+ ProcessorEvent.transaction,
+ ProcessorEvent.error,
+ ProcessorEvent.metric,
+ ],
+ },
+ terminateAfter: 1,
+ body: {
+ size: 0,
+ },
+ };
- const response = await apmEventClient.search(params);
- return response.hits.total.value > 0;
- } catch (e) {
- return false;
- }
- });
+ const response = await apmEventClient.search(
+ 'observability_overview_has_apm_data',
+ 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 79eb0fbce5498..b54e9726667e6 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
@@ -58,6 +58,11 @@ Object {
"transaction.type": "page-load",
},
},
+ Object {
+ "term": Object {
+ "service.environment": "staging",
+ },
+ },
],
},
},
@@ -492,6 +497,11 @@ Object {
"field": "transaction.marks.navigationTiming.fetchStart",
},
},
+ Object {
+ "term": Object {
+ "service.environment": "staging",
+ },
+ },
],
},
},
@@ -534,6 +544,11 @@ Object {
"transaction.type": "page-load",
},
},
+ Object {
+ "term": Object {
+ "service.environment": "staging",
+ },
+ },
],
},
},
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 ea1a65020e0cc..e56f234c0633e 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
@@ -63,7 +63,7 @@ export async function getClientMetrics({
});
const { apmEventClient } = setup;
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search('get_client_metrics', params);
const {
hasFetchStartField: { backEnd, totalPageLoadDuration },
} = response.aggregations!;
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts
index d65399f91bef8..6f734a214501d 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts
@@ -94,7 +94,7 @@ export async function getJSErrors({
const { apmEventClient } = setup;
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search('get_js_errors', params);
const { totalErrorGroups, totalErrorPages, errors } =
response.aggregations ?? {};
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts
index c873b9b4aed82..c4c6f613172d1 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts
@@ -64,7 +64,7 @@ export async function getLongTaskMetrics({
const { apmEventClient } = setup;
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search('get_long_task_metrics', params);
const pkey = percentile.toFixed(1);
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 0f1d7146f8459..73d634e3134d1 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
@@ -117,7 +117,7 @@ export async function getPageLoadDistribution({
const {
aggregations,
hits: { total },
- } = await apmEventClient.search(params);
+ } = await apmEventClient.search('get_page_load_distribution', params);
if (total.value === 0) {
return null;
@@ -210,7 +210,10 @@ const getPercentilesDistribution = async ({
const { apmEventClient } = setup;
- const { aggregations } = await apmEventClient.search(params);
+ const { aggregations } = await apmEventClient.search(
+ 'get_page_load_distribution',
+ params
+ );
return 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 13046e6c5a873..41af2ae166aaf 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
@@ -69,7 +69,7 @@ export async function getPageViewTrends({
const { apmEventClient } = setup;
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search('get_page_view_trends', params);
const { topBreakdowns } = response.aggregations ?? {};
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 6a6caab953733..e63d834307a5f 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
@@ -92,7 +92,10 @@ export const getPageLoadDistBreakdown = async ({
const { apmEventClient } = setup;
- const { aggregations } = await apmEventClient.search(params);
+ const { aggregations } = await apmEventClient.search(
+ 'get_page_load_dist_breakdown',
+ 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 ffe9225f1ab99..a2e6b55738d3a 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
@@ -38,7 +38,7 @@ export async function getRumServices({
const { apmEventClient } = setup;
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search('get_rum_services', params);
const result = response.aggregations?.services.buckets ?? [];
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts
index 7cf9066fb4d4d..ae65cdbd121ea 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts
@@ -56,7 +56,7 @@ export async function getUrlSearch({
const { apmEventClient } = setup;
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search('get_url_search', params);
const { urls, totalUrls } = response.aggregations ?? {};
const pkey = percentile.toFixed(1);
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 247b808896e41..9c7a64d7c6481 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
@@ -51,7 +51,7 @@ export async function getVisitorBreakdown({
const { apmEventClient } = setup;
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search('get_visitor_breakdown', params);
const { browsers, os } = response.aggregations!;
const totalItems = response.hits.total.value;
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts
index 9bde701df5672..bbb301e22aa8d 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts
@@ -103,7 +103,7 @@ export async function getWebCoreVitals({
const { apmEventClient } = setup;
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search('get_web_core_vitals', params);
const {
lcp,
cls,
diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
index 87136fc0538a6..fc5da4ec1d0fa 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
@@ -51,7 +51,7 @@ export async function hasRumData({
const { apmEventClient } = setup;
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search('has_rum_data', params);
return {
indices: setup.indices['apm_oss.transactionIndices']!,
hasData: response.hits.total.value > 0,
diff --git a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
index aaf6401b9f407..452f451b11f86 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
@@ -16,6 +16,7 @@ import { getRumServices } from './get_rum_services';
import { getLongTaskMetrics } from './get_long_task_metrics';
import { getWebCoreVitals } from './get_web_core_vitals';
import { getJSErrors } from './get_js_errors';
+import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
describe('rum client dashboard queries', () => {
let mock: SearchParamsMock;
@@ -25,32 +26,38 @@ describe('rum client dashboard queries', () => {
});
it('fetches client metrics', async () => {
- mock = await inspectSearchParams((setup) =>
- getClientMetrics({
- setup,
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getClientMetrics({
+ setup,
+ }),
+ { uiFilters: { environment: 'staging' } }
);
expect(mock.params).toMatchSnapshot();
});
it('fetches page view trends', async () => {
- mock = await inspectSearchParams((setup) =>
- getPageViewTrends({
- setup,
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getPageViewTrends({
+ setup,
+ }),
+ { uiFilters: { environment: 'staging' } }
);
expect(mock.params).toMatchSnapshot();
});
it('fetches page load distribution', async () => {
- mock = await inspectSearchParams((setup) =>
- getPageLoadDistribution({
- setup,
- minPercentile: '0',
- maxPercentile: '99',
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getPageLoadDistribution({
+ setup,
+ minPercentile: '0',
+ maxPercentile: '99',
+ }),
+ { uiFilters: { environment: 'staging' } }
);
expect(mock.params).toMatchSnapshot();
});
@@ -65,10 +72,12 @@ describe('rum client dashboard queries', () => {
});
it('fetches rum core vitals', async () => {
- mock = await inspectSearchParams((setup) =>
- getWebCoreVitals({
- setup,
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getWebCoreVitals({
+ setup,
+ }),
+ { uiFilters: { environment: ENVIRONMENT_ALL.value } }
);
expect(mock.params).toMatchSnapshot();
});
diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
index 43cbb485c4510..6b8cee244a192 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
@@ -8,6 +8,7 @@
import { ESFilter } from '../../../../../../../typings/elasticsearch';
import { UIFilters } from '../../../../typings/ui_filters';
import { localUIFilters, localUIFilterNames } from './local_ui_filters/config';
+import { environmentQuery } from '../../../utils/queries';
export function getEsFilter(uiFilters: UIFilters) {
const localFilterValues = uiFilters;
@@ -23,5 +24,5 @@ export function getEsFilter(uiFilters: UIFilters) {
};
}) as ESFilter[];
- return mappedFilters;
+ return [...mappedFilters, ...environmentQuery(uiFilters.environment)];
}
diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts
index 8fdeb77171862..e0e9bb2ca002f 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts
@@ -38,37 +38,38 @@ export function getLocalUIFilters({
delete projectionWithoutAggs.body.aggs;
return Promise.all(
- localFilterNames.map(async (name) =>
- withApmSpan('get_ui_filter_options_for_field', async () => {
- const query = getLocalFilterQuery({
- uiFilters,
- projection,
- localUIFilterName: name,
- });
+ localFilterNames.map(async (name) => {
+ const query = getLocalFilterQuery({
+ uiFilters,
+ projection,
+ localUIFilterName: name,
+ });
- const response = await apmEventClient.search(query);
+ const response = await apmEventClient.search(
+ 'get_ui_filter_options_for_field',
+ query
+ );
- const filter = localUIFilters[name];
+ const filter = localUIFilters[name];
- const buckets = response?.aggregations?.by_terms?.buckets ?? [];
+ const buckets = response?.aggregations?.by_terms?.buckets ?? [];
- return {
- ...filter,
- options: orderBy(
- buckets.map((bucket) => {
- return {
- name: bucket.key as string,
- count: bucket.bucket_count
- ? bucket.bucket_count.value
- : bucket.doc_count,
- };
- }),
- 'count',
- 'desc'
- ),
- };
- })
- )
+ return {
+ ...filter,
+ options: orderBy(
+ buckets.map((bucket) => {
+ return {
+ name: bucket.key as string,
+ count: bucket.bucket_count
+ ? bucket.bucket_count.value
+ : bucket.doc_count,
+ };
+ }),
+ 'count',
+ 'desc'
+ ),
+ };
+ })
);
});
}
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 6047b97651e6a..6ecfe425dc8c5 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
@@ -14,44 +14,42 @@ import {
ServiceConnectionNode,
} from '../../../common/service_map';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
-import { withApmSpan } from '../../utils/with_apm_span';
export async function fetchServicePathsFromTraceIds(
setup: Setup & SetupTimeRange,
traceIds: string[]
) {
- return withApmSpan('get_service_paths_from_trace_ids', async () => {
- const { apmEventClient } = setup;
-
- // make sure there's a range so ES can skip shards
- const dayInMs = 24 * 60 * 60 * 1000;
- const start = setup.start - dayInMs;
- const end = setup.end + dayInMs;
-
- const serviceMapParams = {
- apm: {
- events: [ProcessorEvent.span, ProcessorEvent.transaction],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- {
- terms: {
- [TRACE_ID]: traceIds,
- },
+ const { apmEventClient } = setup;
+
+ // make sure there's a range so ES can skip shards
+ const dayInMs = 24 * 60 * 60 * 1000;
+ const start = setup.start - dayInMs;
+ const end = setup.end + dayInMs;
+
+ const serviceMapParams = {
+ apm: {
+ events: [ProcessorEvent.span, ProcessorEvent.transaction],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ {
+ terms: {
+ [TRACE_ID]: traceIds,
},
- ...rangeQuery(start, end),
- ],
- },
+ },
+ ...rangeQuery(start, end),
+ ],
},
- aggs: {
- service_map: {
- scripted_metric: {
- init_script: {
- lang: 'painless',
- source: `state.eventsById = new HashMap();
+ },
+ aggs: {
+ service_map: {
+ scripted_metric: {
+ init_script: {
+ lang: 'painless',
+ source: `state.eventsById = new HashMap();
String[] fieldsToCopy = new String[] {
'parent.id',
@@ -65,10 +63,10 @@ export async function fetchServicePathsFromTraceIds(
'agent.name'
};
state.fieldsToCopy = fieldsToCopy;`,
- },
- map_script: {
- lang: 'painless',
- source: `def id;
+ },
+ map_script: {
+ lang: 'painless',
+ source: `def id;
if (!doc['span.id'].empty) {
id = doc['span.id'].value;
} else {
@@ -85,14 +83,14 @@ export async function fetchServicePathsFromTraceIds(
}
state.eventsById[id] = copy`,
- },
- combine_script: {
- lang: 'painless',
- source: `return state.eventsById;`,
- },
- reduce_script: {
- lang: 'painless',
- source: `
+ },
+ combine_script: {
+ lang: 'painless',
+ source: `return state.eventsById;`,
+ },
+ reduce_script: {
+ lang: 'painless',
+ source: `
def getDestination ( def event ) {
def destination = new HashMap();
destination['span.destination.service.resource'] = event['span.destination.service.resource'];
@@ -208,29 +206,29 @@ export async function fetchServicePathsFromTraceIds(
response.discoveredServices = discoveredServices;
return response;`,
- },
},
},
} as const,
},
- };
-
- const serviceMapFromTraceIdsScriptResponse = await apmEventClient.search(
- serviceMapParams
- );
-
- return serviceMapFromTraceIdsScriptResponse as {
- aggregations?: {
- service_map: {
- value: {
- paths: ConnectionNode[][];
- discoveredServices: Array<{
- from: ExternalConnectionNode;
- to: ServiceConnectionNode;
- }>;
- };
+ },
+ };
+
+ const serviceMapFromTraceIdsScriptResponse = await apmEventClient.search(
+ 'get_service_paths_from_trace_ids',
+ serviceMapParams
+ );
+
+ return serviceMapFromTraceIdsScriptResponse as {
+ aggregations?: {
+ service_map: {
+ value: {
+ paths: ConnectionNode[][];
+ discoveredServices: Array<{
+ from: ExternalConnectionNode;
+ to: ServiceConnectionNode;
+ }>;
};
};
};
- });
+ };
}
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 e5b0b72b8784a..6d50023d3fd0e 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
@@ -87,69 +87,70 @@ async function getConnectionData({
}
async function getServicesData(options: IEnvOptions) {
- return withApmSpan('get_service_stats_for_service_map', async () => {
- const { environment, setup, searchAggregatedTransactions } = options;
+ const { environment, setup, searchAggregatedTransactions } = options;
- const projection = getServicesProjection({
- setup,
- searchAggregatedTransactions,
- });
+ const projection = getServicesProjection({
+ setup,
+ searchAggregatedTransactions,
+ });
- let filter = [
- ...projection.body.query.bool.filter,
- ...environmentQuery(environment),
- ];
+ let filter = [
+ ...projection.body.query.bool.filter,
+ ...environmentQuery(environment),
+ ];
- if (options.serviceName) {
- filter = filter.concat({
- term: {
- [SERVICE_NAME]: options.serviceName,
+ if (options.serviceName) {
+ filter = filter.concat({
+ term: {
+ [SERVICE_NAME]: options.serviceName,
+ },
+ });
+ }
+
+ const params = mergeProjection(projection, {
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ ...projection.body.query.bool,
+ filter,
},
- });
- }
-
- const params = mergeProjection(projection, {
- body: {
- size: 0,
- query: {
- bool: {
- ...projection.body.query.bool,
- filter,
+ },
+ aggs: {
+ services: {
+ terms: {
+ field: projection.body.aggs.services.terms.field,
+ size: 500,
},
- },
- aggs: {
- services: {
- terms: {
- field: projection.body.aggs.services.terms.field,
- size: 500,
- },
- aggs: {
- agent_name: {
- terms: {
- field: AGENT_NAME,
- },
+ aggs: {
+ agent_name: {
+ terms: {
+ field: AGENT_NAME,
},
},
},
},
},
- });
+ },
+ });
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search(
+ 'get_service_stats_for_service_map',
+ params
+ );
- return (
- response.aggregations?.services.buckets.map((bucket) => {
- return {
- [SERVICE_NAME]: bucket.key as string,
- [AGENT_NAME]:
- (bucket.agent_name.buckets[0]?.key as string | undefined) || '',
- [SERVICE_ENVIRONMENT]: options.environment || null,
- };
- }) || []
- );
- });
+ return (
+ response.aggregations?.services.buckets.map((bucket) => {
+ return {
+ [SERVICE_NAME]: bucket.key as string,
+ [AGENT_NAME]:
+ (bucket.agent_name.buckets[0]?.key as string | undefined) || '',
+ [SERVICE_ENVIRONMENT]: options.environment || null,
+ };
+ }) || []
+ );
}
export type ConnectionsResponse = PromiseReturnType;
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 9850c36c573dd..2709fb640d8ce 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
@@ -120,7 +120,7 @@ async function getErrorStats({
});
}
-function getTransactionStats({
+async function getTransactionStats({
setup,
filter,
minutes,
@@ -129,68 +129,70 @@ function getTransactionStats({
avgTransactionDuration: number | null;
avgRequestsPerMinute: number | null;
}> {
- return withApmSpan('get_transaction_stats_for_service_map_node', async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const params = {
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- ...filter,
- ...getDocumentTypeFilterForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- {
- terms: {
- [TRANSACTION_TYPE]: [
- TRANSACTION_REQUEST,
- TRANSACTION_PAGE_LOAD,
- ],
- },
+ const params = {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ ...filter,
+ ...getDocumentTypeFilterForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ {
+ terms: {
+ [TRANSACTION_TYPE]: [
+ TRANSACTION_REQUEST,
+ TRANSACTION_PAGE_LOAD,
+ ],
},
- ],
- },
- },
- track_total_hits: true,
- aggs: {
- duration: {
- avg: {
- field: getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- ),
},
+ ],
+ },
+ },
+ track_total_hits: true,
+ aggs: {
+ duration: {
+ avg: {
+ field: getTransactionDurationFieldForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
},
},
},
- };
- const response = await apmEventClient.search(params);
+ },
+ };
+ const response = await apmEventClient.search(
+ 'get_transaction_stats_for_service_map_node',
+ params
+ );
- const totalRequests = response.hits.total.value;
+ const totalRequests = response.hits.total.value;
- return {
- avgTransactionDuration: response.aggregations?.duration.value ?? null,
- avgRequestsPerMinute: totalRequests > 0 ? totalRequests / minutes : null,
- };
- });
+ return {
+ avgTransactionDuration: response.aggregations?.duration.value ?? null,
+ avgRequestsPerMinute: totalRequests > 0 ? totalRequests / minutes : null,
+ };
}
-function getCpuStats({
+async function getCpuStats({
setup,
filter,
}: TaskParameters): Promise<{ avgCpuUsage: number | null }> {
- return withApmSpan('get_avg_cpu_usage_for_service_map_node', async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const response = await apmEventClient.search({
+ const response = await apmEventClient.search(
+ 'get_avg_cpu_usage_for_service_map_node',
+ {
apm: {
events: [ProcessorEvent.metric],
},
@@ -206,10 +208,10 @@ function getCpuStats({
},
aggs: { avgCpuUsage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } } },
},
- });
+ }
+ );
- return { avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null };
- });
+ return { avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null };
}
function getMemoryStats({
@@ -219,7 +221,7 @@ function getMemoryStats({
return withApmSpan('get_memory_stats_for_service_map_node', async () => {
const { apmEventClient } = setup;
- const getAvgMemoryUsage = ({
+ const getAvgMemoryUsage = async ({
additionalFilters,
script,
}: {
@@ -228,8 +230,9 @@ function getMemoryStats({
| typeof percentCgroupMemoryUsedScript
| typeof percentSystemMemoryUsedScript;
}) => {
- return withApmSpan('get_avg_memory_for_service_map_node', async () => {
- const response = await apmEventClient.search({
+ const response = await apmEventClient.search(
+ 'get_avg_memory_for_service_map_node',
+ {
apm: {
events: [ProcessorEvent.metric],
},
@@ -244,9 +247,9 @@ function getMemoryStats({
avgMemoryUsage: { avg: { script } },
},
},
- });
- return response.aggregations?.avgMemoryUsage.value ?? null;
- });
+ }
+ );
+ return response.aggregations?.avgMemoryUsage.value ?? null;
};
let avgMemoryUsage = await getAvgMemoryUsage({
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 fa04b963388b2..7894a95cf4d7e 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
@@ -18,12 +18,11 @@ import {
import { ProcessorEvent } from '../../../common/processor_event';
import { SERVICE_MAP_TIMEOUT_ERROR } from '../../../common/service_map';
import { environmentQuery, rangeQuery } from '../../../server/utils/queries';
-import { withApmSpan } from '../../utils/with_apm_span';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
const MAX_TRACES_TO_INSPECT = 1000;
-export function getTraceSampleIds({
+export async function getTraceSampleIds({
serviceName,
environment,
setup,
@@ -32,90 +31,88 @@ export function getTraceSampleIds({
environment?: string;
setup: Setup & SetupTimeRange;
}) {
- return withApmSpan('get_trace_sample_ids', async () => {
- const { start, end, apmEventClient, config } = setup;
+ const { start, end, apmEventClient, config } = setup;
- const query = {
- bool: {
- filter: [
- {
- exists: {
- field: SPAN_DESTINATION_SERVICE_RESOURCE,
- },
+ const query = {
+ bool: {
+ filter: [
+ {
+ exists: {
+ field: SPAN_DESTINATION_SERVICE_RESOURCE,
},
- ...rangeQuery(start, end),
- ] as ESFilter[],
- },
- } as { bool: { filter: ESFilter[]; must_not?: ESFilter[] | ESFilter } };
+ },
+ ...rangeQuery(start, end),
+ ] as ESFilter[],
+ },
+ } as { bool: { filter: ESFilter[]; must_not?: ESFilter[] | ESFilter } };
- if (serviceName) {
- query.bool.filter.push({ term: { [SERVICE_NAME]: serviceName } });
- }
+ if (serviceName) {
+ query.bool.filter.push({ term: { [SERVICE_NAME]: serviceName } });
+ }
- query.bool.filter.push(...environmentQuery(environment));
+ query.bool.filter.push(...environmentQuery(environment));
- const fingerprintBucketSize = serviceName
- ? config['xpack.apm.serviceMapFingerprintBucketSize']
- : config['xpack.apm.serviceMapFingerprintGlobalBucketSize'];
+ const fingerprintBucketSize = serviceName
+ ? config['xpack.apm.serviceMapFingerprintBucketSize']
+ : config['xpack.apm.serviceMapFingerprintGlobalBucketSize'];
- const traceIdBucketSize = serviceName
- ? config['xpack.apm.serviceMapTraceIdBucketSize']
- : config['xpack.apm.serviceMapTraceIdGlobalBucketSize'];
+ const traceIdBucketSize = serviceName
+ ? config['xpack.apm.serviceMapTraceIdBucketSize']
+ : config['xpack.apm.serviceMapTraceIdGlobalBucketSize'];
- const samplerShardSize = traceIdBucketSize * 10;
+ const samplerShardSize = traceIdBucketSize * 10;
- const params = {
- apm: {
- events: [ProcessorEvent.span],
- },
- body: {
- size: 0,
- query,
- aggs: {
- connections: {
- composite: {
- sources: asMutableArray([
- {
- [SPAN_DESTINATION_SERVICE_RESOURCE]: {
- terms: {
- field: SPAN_DESTINATION_SERVICE_RESOURCE,
- },
+ const params = {
+ apm: {
+ events: [ProcessorEvent.span],
+ },
+ body: {
+ size: 0,
+ query,
+ aggs: {
+ connections: {
+ composite: {
+ sources: asMutableArray([
+ {
+ [SPAN_DESTINATION_SERVICE_RESOURCE]: {
+ terms: {
+ field: SPAN_DESTINATION_SERVICE_RESOURCE,
},
},
- {
- [SERVICE_NAME]: {
- terms: {
- field: SERVICE_NAME,
- },
+ },
+ {
+ [SERVICE_NAME]: {
+ terms: {
+ field: SERVICE_NAME,
},
},
- {
- [SERVICE_ENVIRONMENT]: {
- terms: {
- field: SERVICE_ENVIRONMENT,
- missing_bucket: true,
- },
+ },
+ {
+ [SERVICE_ENVIRONMENT]: {
+ terms: {
+ field: SERVICE_ENVIRONMENT,
+ missing_bucket: true,
},
},
- ] as const),
- size: fingerprintBucketSize,
- },
- aggs: {
- sample: {
- sampler: {
- shard_size: samplerShardSize,
- },
- aggs: {
- trace_ids: {
- terms: {
- field: TRACE_ID,
- size: traceIdBucketSize,
- execution_hint: 'map' as const,
- // remove bias towards large traces by sorting on trace.id
- // which will be random-esque
- order: {
- _key: 'desc' as const,
- },
+ },
+ ] as const),
+ size: fingerprintBucketSize,
+ },
+ aggs: {
+ sample: {
+ sampler: {
+ shard_size: samplerShardSize,
+ },
+ aggs: {
+ trace_ids: {
+ terms: {
+ field: TRACE_ID,
+ size: traceIdBucketSize,
+ execution_hint: 'map' as const,
+ // remove bias towards large traces by sorting on trace.id
+ // which will be random-esque
+ order: {
+ _key: 'desc' as const,
},
},
},
@@ -124,34 +121,36 @@ export function getTraceSampleIds({
},
},
},
- };
+ },
+ };
- try {
- const tracesSampleResponse = await apmEventClient.search(params);
- // make sure at least one trace per composite/connection bucket
- // is queried
- const traceIdsWithPriority =
- tracesSampleResponse.aggregations?.connections.buckets.flatMap(
- (bucket) =>
- bucket.sample.trace_ids.buckets.map((sampleDocBucket, index) => ({
- traceId: sampleDocBucket.key as string,
- priority: index,
- }))
- ) || [];
+ try {
+ const tracesSampleResponse = await apmEventClient.search(
+ 'get_trace_sample_ids',
+ params
+ );
+ // make sure at least one trace per composite/connection bucket
+ // is queried
+ const traceIdsWithPriority =
+ tracesSampleResponse.aggregations?.connections.buckets.flatMap((bucket) =>
+ bucket.sample.trace_ids.buckets.map((sampleDocBucket, index) => ({
+ traceId: sampleDocBucket.key as string,
+ priority: index,
+ }))
+ ) || [];
- const traceIds = take(
- uniq(
- sortBy(traceIdsWithPriority, 'priority').map(({ traceId }) => traceId)
- ),
- MAX_TRACES_TO_INSPECT
- );
+ const traceIds = take(
+ uniq(
+ sortBy(traceIdsWithPriority, 'priority').map(({ traceId }) => traceId)
+ ),
+ MAX_TRACES_TO_INSPECT
+ );
- return { traceIds };
- } catch (error) {
- if ('displayName' in error && error.displayName === 'RequestTimeout') {
- throw Boom.internal(SERVICE_MAP_TIMEOUT_ERROR);
- }
- throw error;
+ return { traceIds };
+ } catch (error) {
+ if ('displayName' in error && error.displayName === 'RequestTimeout') {
+ throw Boom.internal(SERVICE_MAP_TIMEOUT_ERROR);
}
- });
+ throw error;
+ }
}
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 07b7e532d8055..97c553f344205 100644
--- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts
+++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts
@@ -14,10 +14,9 @@ import {
import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes';
import { getServiceNodesProjection } from '../../projections/service_nodes';
import { mergeProjection } from '../../projections/util/merge_projection';
-import { withApmSpan } from '../../utils/with_apm_span';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
-const getServiceNodes = ({
+const getServiceNodes = async ({
kuery,
setup,
serviceName,
@@ -26,69 +25,67 @@ const getServiceNodes = ({
setup: Setup & SetupTimeRange;
serviceName: string;
}) => {
- return withApmSpan('get_service_nodes', async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const projection = getServiceNodesProjection({ kuery, setup, serviceName });
+ const projection = getServiceNodesProjection({ kuery, setup, serviceName });
- const params = mergeProjection(projection, {
- body: {
- aggs: {
- nodes: {
- terms: {
- ...projection.body.aggs.nodes.terms,
- size: 10000,
- missing: SERVICE_NODE_NAME_MISSING,
- },
- aggs: {
- cpu: {
- avg: {
- field: METRIC_PROCESS_CPU_PERCENT,
- },
+ const params = mergeProjection(projection, {
+ body: {
+ aggs: {
+ nodes: {
+ terms: {
+ ...projection.body.aggs.nodes.terms,
+ size: 10000,
+ missing: SERVICE_NODE_NAME_MISSING,
+ },
+ aggs: {
+ cpu: {
+ avg: {
+ field: METRIC_PROCESS_CPU_PERCENT,
},
- heapMemory: {
- avg: {
- field: METRIC_JAVA_HEAP_MEMORY_USED,
- },
+ },
+ heapMemory: {
+ avg: {
+ field: METRIC_JAVA_HEAP_MEMORY_USED,
},
- nonHeapMemory: {
- avg: {
- field: METRIC_JAVA_NON_HEAP_MEMORY_USED,
- },
+ },
+ nonHeapMemory: {
+ avg: {
+ field: METRIC_JAVA_NON_HEAP_MEMORY_USED,
},
- threadCount: {
- max: {
- field: METRIC_JAVA_THREAD_COUNT,
- },
+ },
+ threadCount: {
+ max: {
+ field: METRIC_JAVA_THREAD_COUNT,
},
},
},
},
},
- });
+ },
+ });
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search('get_service_nodes', params);
- if (!response.aggregations) {
- return [];
- }
+ if (!response.aggregations) {
+ return [];
+ }
- return response.aggregations.nodes.buckets
- .map((bucket) => ({
- name: bucket.key as string,
- cpu: bucket.cpu.value,
- heapMemory: bucket.heapMemory.value,
- nonHeapMemory: bucket.nonHeapMemory.value,
- threadCount: bucket.threadCount.value,
- }))
- .filter(
- (item) =>
- item.cpu !== null ||
- item.heapMemory !== null ||
- item.nonHeapMemory !== null ||
- item.threadCount != null
- );
- });
+ return response.aggregations.nodes.buckets
+ .map((bucket) => ({
+ name: bucket.key as string,
+ cpu: bucket.cpu.value,
+ heapMemory: bucket.heapMemory.value,
+ nonHeapMemory: bucket.nonHeapMemory.value,
+ threadCount: bucket.threadCount.value,
+ }))
+ .filter(
+ (item) =>
+ item.cpu !== null ||
+ item.heapMemory !== null ||
+ item.nonHeapMemory !== null ||
+ item.threadCount != null
+ );
};
export { getServiceNodes };
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 9d05369aca840..2f653e2c4df1d 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
@@ -22,6 +22,7 @@ Object {
"events": Array [
"transaction",
],
+ "includeLegacyData": true,
},
"body": Object {
"query": Object {
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 611f9b18a0b1a..202b5075d2ea7 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
@@ -13,7 +13,6 @@ import {
SERVICE_VERSION,
} from '../../../../common/elasticsearch_fieldnames';
import { environmentQuery, rangeQuery } from '../../../../server/utils/queries';
-import { withApmSpan } from '../../../utils/with_apm_span';
import {
getDocumentTypeFilterForAggregatedTransactions,
getProcessorEventForAggregatedTransactions,
@@ -31,20 +30,52 @@ export async function getDerivedServiceAnnotations({
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
}) {
- return withApmSpan('get_derived_service_annotations', async () => {
- const { start, end, apmEventClient } = setup;
+ const { start, end, apmEventClient } = setup;
- const filter: ESFilter[] = [
- { term: { [SERVICE_NAME]: serviceName } },
- ...getDocumentTypeFilterForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ...environmentQuery(environment),
- ];
+ const filter: ESFilter[] = [
+ { term: { [SERVICE_NAME]: serviceName } },
+ ...getDocumentTypeFilterForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ...environmentQuery(environment),
+ ];
- const versions =
- (
- await apmEventClient.search({
+ const versions =
+ (
+ await apmEventClient.search('get_derived_service_annotations', {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [...filter, ...rangeQuery(start, end)],
+ },
+ },
+ aggs: {
+ versions: {
+ terms: {
+ field: SERVICE_VERSION,
+ },
+ },
+ },
+ },
+ })
+ ).aggregations?.versions.buckets.map((bucket) => bucket.key) ?? [];
+
+ if (versions.length <= 1) {
+ return [];
+ }
+ const annotations = await Promise.all(
+ versions.map(async (version) => {
+ const response = await apmEventClient.search(
+ 'get_first_seen_of_version',
+ {
apm: {
events: [
getProcessorEventForAggregatedTransactions(
@@ -53,73 +84,40 @@ export async function getDerivedServiceAnnotations({
],
},
body: {
- size: 0,
+ size: 1,
query: {
bool: {
- filter: [...filter, ...rangeQuery(start, end)],
+ filter: [...filter, { term: { [SERVICE_VERSION]: version } }],
},
},
- aggs: {
- versions: {
- terms: {
- field: SERVICE_VERSION,
- },
- },
+ sort: {
+ '@timestamp': 'asc',
},
},
- })
- ).aggregations?.versions.buckets.map((bucket) => bucket.key) ?? [];
+ }
+ );
- if (versions.length <= 1) {
- return [];
- }
- const annotations = await Promise.all(
- versions.map(async (version) => {
- return withApmSpan('get_first_seen_of_version', async () => {
- const response = await apmEventClient.search({
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ],
- },
- body: {
- size: 1,
- query: {
- bool: {
- filter: [...filter, { term: { [SERVICE_VERSION]: version } }],
- },
- },
- sort: {
- '@timestamp': 'asc',
- },
- },
- });
-
- const firstSeen = new Date(
- response.hits.hits[0]._source['@timestamp']
- ).getTime();
+ const firstSeen = new Date(
+ response.hits.hits[0]._source['@timestamp']
+ ).getTime();
- if (!isFiniteNumber(firstSeen)) {
- throw new Error(
- 'First seen for version was unexpectedly undefined or null.'
- );
- }
+ if (!isFiniteNumber(firstSeen)) {
+ throw new Error(
+ 'First seen for version was unexpectedly undefined or null.'
+ );
+ }
- if (firstSeen < start || firstSeen > end) {
- return null;
- }
+ if (firstSeen < start || firstSeen > end) {
+ return null;
+ }
- return {
- type: AnnotationType.VERSION,
- id: version,
- '@timestamp': firstSeen,
- text: version,
- };
- });
- })
- );
- return annotations.filter(Boolean) as Annotation[];
- });
+ return {
+ type: AnnotationType.VERSION,
+ id: version,
+ '@timestamp': firstSeen,
+ text: version,
+ };
+ })
+ );
+ return annotations.filter(Boolean) as Annotation[];
}
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 a81c0b2fc2c44..82147d7c94236 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
@@ -13,9 +13,8 @@ import {
import { rangeQuery } from '../../../server/utils/queries';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions';
-import { withApmSpan } from '../../utils/with_apm_span';
-export function getServiceAgentName({
+export async function getServiceAgentName({
serviceName,
setup,
searchAggregatedTransactions,
@@ -24,42 +23,41 @@ export function getServiceAgentName({
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
}) {
- return withApmSpan('get_service_agent_name', async () => {
- const { start, end, apmEventClient } = setup;
+ const { start, end, apmEventClient } = setup;
- const params = {
- terminateAfter: 1,
- apm: {
- events: [
- ProcessorEvent.error,
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ProcessorEvent.metric,
- ],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- { term: { [SERVICE_NAME]: serviceName } },
- ...rangeQuery(start, end),
- ],
- },
+ const params = {
+ terminateAfter: 1,
+ apm: {
+ events: [
+ ProcessorEvent.error,
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ProcessorEvent.metric,
+ ],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { term: { [SERVICE_NAME]: serviceName } },
+ ...rangeQuery(start, end),
+ ],
},
- aggs: {
- agents: {
- terms: { field: AGENT_NAME, size: 1 },
- },
+ },
+ aggs: {
+ agents: {
+ terms: { field: AGENT_NAME, size: 1 },
},
},
- };
+ },
+ };
- const { aggregations } = await apmEventClient.search(params);
- const agentName = aggregations?.agents.buckets[0]?.key as
- | string
- | undefined;
- return { agentName };
- });
+ const { aggregations } = await apmEventClient.search(
+ 'get_service_agent_name',
+ 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_dependencies/get_destination_map.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts
index db491012c986b..4993484f5b240 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts
@@ -38,56 +38,54 @@ export const getDestinationMap = ({
return withApmSpan('get_service_destination_map', async () => {
const { start, end, apmEventClient } = setup;
- const response = await withApmSpan('get_exit_span_samples', async () =>
- apmEventClient.search({
- apm: {
- events: [ProcessorEvent.span],
+ const response = await apmEventClient.search('get_exit_span_samples', {
+ apm: {
+ events: [ProcessorEvent.span],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { term: { [SERVICE_NAME]: serviceName } },
+ { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE } },
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ],
+ },
},
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- { term: { [SERVICE_NAME]: serviceName } },
- { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE } },
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ],
+ aggs: {
+ connections: {
+ composite: {
+ size: 1000,
+ sources: asMutableArray([
+ {
+ [SPAN_DESTINATION_SERVICE_RESOURCE]: {
+ terms: { field: SPAN_DESTINATION_SERVICE_RESOURCE },
+ },
+ },
+ // make sure we get samples for both successful
+ // and failed calls
+ { [EVENT_OUTCOME]: { terms: { field: EVENT_OUTCOME } } },
+ ] as const),
},
- },
- aggs: {
- connections: {
- composite: {
- size: 1000,
- sources: asMutableArray([
- {
- [SPAN_DESTINATION_SERVICE_RESOURCE]: {
- terms: { field: SPAN_DESTINATION_SERVICE_RESOURCE },
+ aggs: {
+ sample: {
+ top_hits: {
+ size: 1,
+ _source: [SPAN_TYPE, SPAN_SUBTYPE, SPAN_ID],
+ sort: [
+ {
+ '@timestamp': 'desc' as const,
},
- },
- // make sure we get samples for both successful
- // and failed calls
- { [EVENT_OUTCOME]: { terms: { field: EVENT_OUTCOME } } },
- ] as const),
- },
- aggs: {
- sample: {
- top_hits: {
- size: 1,
- _source: [SPAN_TYPE, SPAN_SUBTYPE, SPAN_ID],
- sort: [
- {
- '@timestamp': 'desc' as const,
- },
- ],
- },
+ ],
},
},
},
},
},
- })
- );
+ },
+ });
const outgoingConnections =
response.aggregations?.connections.buckets.map((bucket) => {
@@ -103,38 +101,37 @@ export const getDestinationMap = ({
};
}) ?? [];
- const transactionResponse = await withApmSpan(
+ const transactionResponse = await apmEventClient.search(
'get_transactions_for_exit_spans',
- () =>
- apmEventClient.search({
- apm: {
- events: [ProcessorEvent.transaction],
- },
- body: {
- query: {
- bool: {
- filter: [
- {
- terms: {
- [PARENT_ID]: outgoingConnections.map(
- (connection) => connection[SPAN_ID]
- ),
- },
+ {
+ apm: {
+ events: [ProcessorEvent.transaction],
+ },
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ terms: {
+ [PARENT_ID]: outgoingConnections.map(
+ (connection) => connection[SPAN_ID]
+ ),
},
- ...rangeQuery(start, end),
- ],
- },
+ },
+ ...rangeQuery(start, end),
+ ],
},
- size: outgoingConnections.length,
- docvalue_fields: asMutableArray([
- SERVICE_NAME,
- SERVICE_ENVIRONMENT,
- AGENT_NAME,
- PARENT_ID,
- ] as const),
- _source: false,
},
- })
+ size: outgoingConnections.length,
+ docvalue_fields: asMutableArray([
+ SERVICE_NAME,
+ SERVICE_ENVIRONMENT,
+ AGENT_NAME,
+ PARENT_ID,
+ ] as const),
+ _source: false,
+ },
+ }
);
const incomingConnections = transactionResponse.hits.hits.map((hit) => ({
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts
index c8642c6272b5f..1d815dd7180e3 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts
@@ -18,9 +18,8 @@ import { environmentQuery, rangeQuery } from '../../../../server/utils/queries';
import { getBucketSize } from '../../helpers/get_bucket_size';
import { EventOutcome } from '../../../../common/event_outcome';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
-import { withApmSpan } from '../../../utils/with_apm_span';
-export const getMetrics = ({
+export const getMetrics = async ({
setup,
serviceName,
environment,
@@ -31,10 +30,11 @@ export const getMetrics = ({
environment?: string;
numBuckets: number;
}) => {
- return withApmSpan('get_service_destination_metrics', async () => {
- const { start, end, apmEventClient } = setup;
+ const { start, end, apmEventClient } = setup;
- const response = await apmEventClient.search({
+ const response = await apmEventClient.search(
+ 'get_service_destination_metrics',
+ {
apm: {
events: [ProcessorEvent.metric],
},
@@ -46,7 +46,9 @@ export const getMetrics = ({
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
{
- exists: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT },
+ exists: {
+ field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
+ },
},
...rangeQuery(start, end),
...environmentQuery(environment),
@@ -99,47 +101,47 @@ export const getMetrics = ({
},
},
},
- });
+ }
+ );
- return (
- response.aggregations?.connections.buckets.map((bucket) => ({
- span: {
- destination: {
- service: {
- resource: String(bucket.key),
- },
+ return (
+ response.aggregations?.connections.buckets.map((bucket) => ({
+ span: {
+ destination: {
+ service: {
+ resource: String(bucket.key),
},
},
- value: {
- count: sum(
- bucket.timeseries.buckets.map(
- (dateBucket) => dateBucket.count.value ?? 0
- )
- ),
- latency_sum: sum(
- bucket.timeseries.buckets.map(
- (dateBucket) => dateBucket.latency_sum.value ?? 0
- )
- ),
- error_count: sum(
- bucket.timeseries.buckets.flatMap(
- (dateBucket) =>
- dateBucket[EVENT_OUTCOME].buckets.find(
- (outcomeBucket) => outcomeBucket.key === EventOutcome.failure
- )?.count.value ?? 0
- )
- ),
- },
- timeseries: bucket.timeseries.buckets.map((dateBucket) => ({
- x: dateBucket.key,
- count: dateBucket.count.value ?? 0,
- latency_sum: dateBucket.latency_sum.value ?? 0,
- error_count:
- dateBucket[EVENT_OUTCOME].buckets.find(
- (outcomeBucket) => outcomeBucket.key === EventOutcome.failure
- )?.count.value ?? 0,
- })),
- })) ?? []
- );
- });
+ },
+ value: {
+ count: sum(
+ bucket.timeseries.buckets.map(
+ (dateBucket) => dateBucket.count.value ?? 0
+ )
+ ),
+ latency_sum: sum(
+ bucket.timeseries.buckets.map(
+ (dateBucket) => dateBucket.latency_sum.value ?? 0
+ )
+ ),
+ error_count: sum(
+ bucket.timeseries.buckets.flatMap(
+ (dateBucket) =>
+ dateBucket[EVENT_OUTCOME].buckets.find(
+ (outcomeBucket) => outcomeBucket.key === EventOutcome.failure
+ )?.count.value ?? 0
+ )
+ ),
+ },
+ timeseries: bucket.timeseries.buckets.map((dateBucket) => ({
+ x: dateBucket.key,
+ count: dateBucket.count.value ?? 0,
+ latency_sum: dateBucket.latency_sum.value ?? 0,
+ error_count:
+ dateBucket[EVENT_OUTCOME].buckets.find(
+ (outcomeBucket) => outcomeBucket.key === EventOutcome.failure
+ )?.count.value ?? 0,
+ })),
+ })) ?? []
+ );
};
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts
index e45864de2fc1e..bd69bfc53db71 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts
@@ -18,7 +18,6 @@ import {
rangeQuery,
kqlQuery,
} from '../../../../server/utils/queries';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { getBucketSize } from '../../helpers/get_bucket_size';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
@@ -43,75 +42,71 @@ export async function getServiceErrorGroupDetailedStatistics({
start: number;
end: number;
}): Promise> {
- return withApmSpan(
- 'get_service_error_group_detailed_statistics',
- async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const { intervalString } = getBucketSize({ start, end, numBuckets });
+ const { intervalString } = getBucketSize({ start, end, numBuckets });
- const timeseriesResponse = await apmEventClient.search({
- apm: {
- events: [ProcessorEvent.error],
+ const timeseriesResponse = await apmEventClient.search(
+ 'get_service_error_group_detailed_statistics',
+ {
+ apm: {
+ events: [ProcessorEvent.error],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { terms: { [ERROR_GROUP_ID]: groupIds } },
+ { term: { [SERVICE_NAME]: serviceName } },
+ { term: { [TRANSACTION_TYPE]: transactionType } },
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ ],
+ },
},
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- { terms: { [ERROR_GROUP_ID]: groupIds } },
- { term: { [SERVICE_NAME]: serviceName } },
- { term: { [TRANSACTION_TYPE]: transactionType } },
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ...kqlQuery(kuery),
- ],
+ aggs: {
+ error_groups: {
+ terms: {
+ field: ERROR_GROUP_ID,
+ size: 500,
},
- },
- aggs: {
- error_groups: {
- terms: {
- field: ERROR_GROUP_ID,
- size: 500,
- },
- aggs: {
- timeseries: {
- date_histogram: {
- field: '@timestamp',
- fixed_interval: intervalString,
- min_doc_count: 0,
- extended_bounds: {
- min: start,
- max: end,
- },
+ aggs: {
+ timeseries: {
+ date_histogram: {
+ field: '@timestamp',
+ fixed_interval: intervalString,
+ min_doc_count: 0,
+ extended_bounds: {
+ min: start,
+ max: end,
},
},
},
},
},
},
- });
-
- if (!timeseriesResponse.aggregations) {
- return [];
- }
-
- return timeseriesResponse.aggregations.error_groups.buckets.map(
- (bucket) => {
- const groupId = bucket.key as string;
- return {
- groupId,
- timeseries: bucket.timeseries.buckets.map((timeseriesBucket) => {
- return {
- x: timeseriesBucket.key,
- y: timeseriesBucket.doc_count,
- };
- }),
- };
- }
- );
+ },
}
);
+
+ if (!timeseriesResponse.aggregations) {
+ return [];
+ }
+
+ return timeseriesResponse.aggregations.error_groups.buckets.map((bucket) => {
+ const groupId = bucket.key as string;
+ return {
+ groupId,
+ timeseries: bucket.timeseries.buckets.map((timeseriesBucket) => {
+ return {
+ x: timeseriesBucket.key,
+ y: timeseriesBucket.doc_count,
+ };
+ }),
+ };
+ });
}
export async function getServiceErrorGroupPeriods({
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts
index 361c92244aee0..8168c0d5549aa 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts
@@ -19,11 +19,10 @@ import {
rangeQuery,
kqlQuery,
} from '../../../../server/utils/queries';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { getErrorName } from '../../helpers/get_error_name';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
-export function getServiceErrorGroupMainStatistics({
+export async function getServiceErrorGroupMainStatistics({
kuery,
serviceName,
setup,
@@ -36,10 +35,11 @@ export function getServiceErrorGroupMainStatistics({
transactionType: string;
environment?: string;
}) {
- return withApmSpan('get_service_error_group_main_statistics', async () => {
- const { apmEventClient, start, end } = setup;
+ const { apmEventClient, start, end } = setup;
- const response = await apmEventClient.search({
+ const response = await apmEventClient.search(
+ 'get_service_error_group_main_statistics',
+ {
apm: {
events: [ProcessorEvent.error],
},
@@ -79,24 +79,23 @@ export function getServiceErrorGroupMainStatistics({
},
},
},
- });
+ }
+ );
- const errorGroups =
- response.aggregations?.error_groups.buckets.map((bucket) => ({
- group_id: bucket.key as string,
- name:
- getErrorName(bucket.sample.hits.hits[0]._source) ??
- NOT_AVAILABLE_LABEL,
- last_seen: new Date(
- bucket.sample.hits.hits[0]?._source['@timestamp']
- ).getTime(),
- occurrences: bucket.doc_count,
- })) ?? [];
+ const errorGroups =
+ response.aggregations?.error_groups.buckets.map((bucket) => ({
+ group_id: bucket.key as string,
+ name:
+ getErrorName(bucket.sample.hits.hits[0]._source) ?? NOT_AVAILABLE_LABEL,
+ last_seen: new Date(
+ bucket.sample.hits.hits[0]?._source['@timestamp']
+ ).getTime(),
+ occurrences: bucket.doc_count,
+ })) ?? [];
- return {
- is_aggregation_accurate:
- (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0,
- error_groups: errorGroups,
- };
- });
+ return {
+ is_aggregation_accurate:
+ (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0,
+ error_groups: errorGroups,
+ };
}
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts
index 7729822df30ca..b720c56464c30 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts
@@ -59,8 +59,9 @@ export async function getServiceErrorGroups({
const { intervalString } = getBucketSize({ start, end, numBuckets });
- const response = await withApmSpan('get_top_service_error_groups', () =>
- apmEventClient.search({
+ const response = await apmEventClient.search(
+ 'get_top_service_error_groups',
+ {
apm: {
events: [ProcessorEvent.error],
},
@@ -104,7 +105,7 @@ export async function getServiceErrorGroups({
},
},
},
- })
+ }
);
const errorGroups =
@@ -139,50 +140,49 @@ export async function getServiceErrorGroups({
(group) => group.group_id
);
- const timeseriesResponse = await withApmSpan(
+ const timeseriesResponse = await apmEventClient.search(
'get_service_error_groups_timeseries',
- async () =>
- apmEventClient.search({
- apm: {
- events: [ProcessorEvent.error],
+ {
+ apm: {
+ events: [ProcessorEvent.error],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { terms: { [ERROR_GROUP_ID]: sortedErrorGroupIds } },
+ { term: { [SERVICE_NAME]: serviceName } },
+ { term: { [TRANSACTION_TYPE]: transactionType } },
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ ],
+ },
},
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- { terms: { [ERROR_GROUP_ID]: sortedErrorGroupIds } },
- { term: { [SERVICE_NAME]: serviceName } },
- { term: { [TRANSACTION_TYPE]: transactionType } },
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ...kqlQuery(kuery),
- ],
+ aggs: {
+ error_groups: {
+ terms: {
+ field: ERROR_GROUP_ID,
+ size,
},
- },
- aggs: {
- error_groups: {
- terms: {
- field: ERROR_GROUP_ID,
- size,
- },
- aggs: {
- timeseries: {
- date_histogram: {
- field: '@timestamp',
- fixed_interval: intervalString,
- min_doc_count: 0,
- extended_bounds: {
- min: start,
- max: end,
- },
+ aggs: {
+ timeseries: {
+ date_histogram: {
+ field: '@timestamp',
+ fixed_interval: intervalString,
+ min_doc_count: 0,
+ extended_bounds: {
+ min: start,
+ max: end,
},
},
},
},
},
},
- })
+ },
+ }
);
return {
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instance_metadata_details.ts b/x-pack/plugins/apm/server/lib/services/get_service_instance_metadata_details.ts
index 25935bcc37dff..bdf9530a9c0c7 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_instance_metadata_details.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_instance_metadata_details.ts
@@ -11,7 +11,6 @@ import {
TRANSACTION_TYPE,
} from '../../../common/elasticsearch_fieldnames';
import { environmentQuery, kqlQuery, rangeQuery } from '../../utils/queries';
-import { withApmSpan } from '../../utils/with_apm_span';
import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
@@ -37,18 +36,19 @@ export async function getServiceInstanceMetadataDetails({
environment?: string;
kuery?: string;
}) {
- return withApmSpan('get_service_instance_metadata_details', async () => {
- const { start, end, apmEventClient } = setup;
- const filter = [
- { term: { [SERVICE_NAME]: serviceName } },
- { term: { [SERVICE_NODE_NAME]: serviceNodeName } },
- { term: { [TRANSACTION_TYPE]: transactionType } },
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ...kqlQuery(kuery),
- ];
+ const { start, end, apmEventClient } = setup;
+ const filter = [
+ { term: { [SERVICE_NAME]: serviceName } },
+ { term: { [SERVICE_NODE_NAME]: serviceNodeName } },
+ { term: { [TRANSACTION_TYPE]: transactionType } },
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ ];
- const response = await apmEventClient.search({
+ const response = await apmEventClient.search(
+ 'get_service_instance_metadata_details',
+ {
apm: {
events: [
getProcessorEventForAggregatedTransactions(
@@ -61,24 +61,24 @@ export async function getServiceInstanceMetadataDetails({
size: 1,
query: { bool: { filter } },
},
- });
+ }
+ );
- const sample = response.hits.hits[0]?._source;
+ const sample = response.hits.hits[0]?._source;
- if (!sample) {
- return {};
- }
+ if (!sample) {
+ return {};
+ }
- const { agent, service, container, kubernetes, host, cloud } = sample;
+ const { agent, service, container, kubernetes, host, cloud } = sample;
- return {
- '@timestamp': sample['@timestamp'],
- agent,
- service,
- container,
- kubernetes,
- host,
- cloud,
- };
- });
+ return {
+ '@timestamp': sample['@timestamp'],
+ agent,
+ service,
+ container,
+ kubernetes,
+ host,
+ cloud,
+ };
}
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts
index 1a33e9810dd5e..526ae19143f13 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts
@@ -24,7 +24,6 @@ import {
percentCgroupMemoryUsedScript,
percentSystemMemoryUsedScript,
} from '../../metrics/by_agent/shared/memory';
-import { withApmSpan } from '../../../utils/with_apm_span';
interface ServiceInstanceSystemMetricPrimaryStatistics {
serviceNodeName: string;
@@ -67,142 +66,140 @@ export async function getServiceInstancesSystemMetricStatistics<
size?: number;
isComparisonSearch: T;
}): Promise>> {
- return withApmSpan(
- 'get_service_instances_system_metric_statistics',
- async () => {
- const { apmEventClient } = setup;
-
- const { intervalString } = getBucketSize({ start, end, numBuckets });
-
- const systemMemoryFilter = {
- bool: {
- filter: [
- { exists: { field: METRIC_SYSTEM_FREE_MEMORY } },
- { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
- ],
- },
- };
-
- const cgroupMemoryFilter = {
- exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES },
- };
-
- const cpuUsageFilter = { exists: { field: METRIC_PROCESS_CPU_PERCENT } };
-
- function withTimeseries(
- agg: TParams
- ) {
- return {
- ...(isComparisonSearch
- ? {
- avg: { avg: agg },
- timeseries: {
- date_histogram: {
- field: '@timestamp',
- fixed_interval: intervalString,
- min_doc_count: 0,
- extended_bounds: {
- min: start,
- max: end,
- },
- },
- aggs: { avg: { avg: agg } },
+ const { apmEventClient } = setup;
+
+ const { intervalString } = getBucketSize({ start, end, numBuckets });
+
+ const systemMemoryFilter = {
+ bool: {
+ filter: [
+ { exists: { field: METRIC_SYSTEM_FREE_MEMORY } },
+ { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
+ ],
+ },
+ };
+
+ const cgroupMemoryFilter = {
+ exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES },
+ };
+
+ const cpuUsageFilter = { exists: { field: METRIC_PROCESS_CPU_PERCENT } };
+
+ function withTimeseries(
+ agg: TParams
+ ) {
+ return {
+ ...(isComparisonSearch
+ ? {
+ avg: { avg: agg },
+ timeseries: {
+ date_histogram: {
+ field: '@timestamp',
+ fixed_interval: intervalString,
+ min_doc_count: 0,
+ extended_bounds: {
+ min: start,
+ max: end,
},
- }
- : { avg: { avg: agg } }),
- };
- }
-
- const subAggs = {
- memory_usage_cgroup: {
- filter: cgroupMemoryFilter,
- aggs: withTimeseries({ script: percentCgroupMemoryUsedScript }),
- },
- memory_usage_system: {
- filter: systemMemoryFilter,
- aggs: withTimeseries({ script: percentSystemMemoryUsedScript }),
- },
- cpu_usage: {
- filter: cpuUsageFilter,
- aggs: withTimeseries({ field: METRIC_PROCESS_CPU_PERCENT }),
- },
- };
-
- const response = await apmEventClient.search({
- apm: {
- events: [ProcessorEvent.metric],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- { term: { [SERVICE_NAME]: serviceName } },
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ...kqlQuery(kuery),
- ...(isComparisonSearch && serviceNodeIds
- ? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }]
- : []),
- ],
- should: [cgroupMemoryFilter, systemMemoryFilter, cpuUsageFilter],
- minimum_should_match: 1,
+ },
+ aggs: { avg: { avg: agg } },
},
+ }
+ : { avg: { avg: agg } }),
+ };
+ }
+
+ const subAggs = {
+ memory_usage_cgroup: {
+ filter: cgroupMemoryFilter,
+ aggs: withTimeseries({ script: percentCgroupMemoryUsedScript }),
+ },
+ memory_usage_system: {
+ filter: systemMemoryFilter,
+ aggs: withTimeseries({ script: percentSystemMemoryUsedScript }),
+ },
+ cpu_usage: {
+ filter: cpuUsageFilter,
+ aggs: withTimeseries({ field: METRIC_PROCESS_CPU_PERCENT }),
+ },
+ };
+
+ const response = await apmEventClient.search(
+ 'get_service_instances_system_metric_statistics',
+ {
+ apm: {
+ events: [ProcessorEvent.metric],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { term: { [SERVICE_NAME]: serviceName } },
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ ...(isComparisonSearch && serviceNodeIds
+ ? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }]
+ : []),
+ ],
+ should: [cgroupMemoryFilter, systemMemoryFilter, cpuUsageFilter],
+ minimum_should_match: 1,
},
- aggs: {
- [SERVICE_NODE_NAME]: {
- terms: {
- field: SERVICE_NODE_NAME,
- missing: SERVICE_NODE_NAME_MISSING,
- ...(size ? { size } : {}),
- ...(isComparisonSearch ? { include: serviceNodeIds } : {}),
- },
- aggs: subAggs,
+ },
+ aggs: {
+ [SERVICE_NODE_NAME]: {
+ terms: {
+ field: SERVICE_NODE_NAME,
+ missing: SERVICE_NODE_NAME_MISSING,
+ ...(size ? { size } : {}),
+ ...(isComparisonSearch ? { include: serviceNodeIds } : {}),
},
+ aggs: subAggs,
},
},
- });
-
- return (
- (response.aggregations?.[SERVICE_NODE_NAME].buckets.map(
- (serviceNodeBucket) => {
- const serviceNodeName = String(serviceNodeBucket.key);
- const hasCGroupData =
- serviceNodeBucket.memory_usage_cgroup.avg.value !== null;
-
- const memoryMetricsKey = hasCGroupData
- ? 'memory_usage_cgroup'
- : 'memory_usage_system';
-
- const cpuUsage =
- // Timeseries is available when isComparisonSearch is true
- 'timeseries' in serviceNodeBucket.cpu_usage
- ? serviceNodeBucket.cpu_usage.timeseries.buckets.map(
- (dateBucket) => ({
- x: dateBucket.key,
- y: dateBucket.avg.value,
- })
- )
- : serviceNodeBucket.cpu_usage.avg.value;
-
- const memoryUsageValue = serviceNodeBucket[memoryMetricsKey];
- const memoryUsage =
- // Timeseries is available when isComparisonSearch is true
- 'timeseries' in memoryUsageValue
- ? memoryUsageValue.timeseries.buckets.map((dateBucket) => ({
- x: dateBucket.key,
- y: dateBucket.avg.value,
- }))
- : serviceNodeBucket[memoryMetricsKey].avg.value;
-
- return {
- serviceNodeName,
- cpuUsage,
- memoryUsage,
- };
- }
- ) as Array>) || []
- );
+ },
}
);
+
+ return (
+ (response.aggregations?.[SERVICE_NODE_NAME].buckets.map(
+ (serviceNodeBucket) => {
+ const serviceNodeName = String(serviceNodeBucket.key);
+ const hasCGroupData =
+ serviceNodeBucket.memory_usage_cgroup.avg.value !== null;
+
+ const memoryMetricsKey = hasCGroupData
+ ? 'memory_usage_cgroup'
+ : 'memory_usage_system';
+
+ const cpuUsage =
+ // Timeseries is available when isComparisonSearch is true
+ 'timeseries' in serviceNodeBucket.cpu_usage
+ ? serviceNodeBucket.cpu_usage.timeseries.buckets.map(
+ (dateBucket) => ({
+ x: dateBucket.key,
+ y: dateBucket.avg.value,
+ })
+ )
+ : serviceNodeBucket.cpu_usage.avg.value;
+
+ const memoryUsageValue = serviceNodeBucket[memoryMetricsKey];
+ const memoryUsage =
+ // Timeseries is available when isComparisonSearch is true
+ 'timeseries' in memoryUsageValue
+ ? memoryUsageValue.timeseries.buckets.map((dateBucket) => ({
+ x: dateBucket.key,
+ y: dateBucket.avg.value,
+ }))
+ : serviceNodeBucket[memoryMetricsKey].avg.value;
+
+ return {
+ serviceNodeName,
+ cpuUsage,
+ memoryUsage,
+ };
+ }
+ ) as Array>) || []
+ );
}
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts
index ad54a231b52ef..7d9dca9b2a706 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts
@@ -26,7 +26,6 @@ import {
getLatencyValue,
} from '../../helpers/latency_aggregation_type';
import { Setup } from '../../helpers/setup_request';
-import { withApmSpan } from '../../../utils/with_apm_span';
interface ServiceInstanceTransactionPrimaryStatistics {
serviceNodeName: string;
@@ -77,126 +76,124 @@ export async function getServiceInstancesTransactionStatistics<
size?: number;
numBuckets?: number;
}): Promise>> {
- return withApmSpan(
- 'get_service_instances_transaction_statistics',
- async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const { intervalString, bucketSize } = getBucketSize({
- start,
- end,
- numBuckets,
- });
+ const { intervalString, bucketSize } = getBucketSize({
+ start,
+ end,
+ numBuckets,
+ });
- const field = getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- );
+ const field = getTransactionDurationFieldForAggregatedTransactions(
+ searchAggregatedTransactions
+ );
- const subAggs = {
- ...getLatencyAggregation(latencyAggregationType, field),
- failures: {
- filter: {
- term: {
- [EVENT_OUTCOME]: EventOutcome.failure,
- },
- },
+ const subAggs = {
+ ...getLatencyAggregation(latencyAggregationType, field),
+ failures: {
+ filter: {
+ term: {
+ [EVENT_OUTCOME]: EventOutcome.failure,
},
- };
+ },
+ },
+ };
- const query = {
- bool: {
- filter: [
- { term: { [SERVICE_NAME]: serviceName } },
- { term: { [TRANSACTION_TYPE]: transactionType } },
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ...kqlQuery(kuery),
- ...(isComparisonSearch && serviceNodeIds
- ? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }]
- : []),
- ],
- },
- };
+ const query = {
+ bool: {
+ filter: [
+ { term: { [SERVICE_NAME]: serviceName } },
+ { term: { [TRANSACTION_TYPE]: transactionType } },
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ ...(isComparisonSearch && serviceNodeIds
+ ? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }]
+ : []),
+ ],
+ },
+ };
- const aggs = {
- [SERVICE_NODE_NAME]: {
- terms: {
- field: SERVICE_NODE_NAME,
- missing: SERVICE_NODE_NAME_MISSING,
- ...(size ? { size } : {}),
- ...(isComparisonSearch ? { include: serviceNodeIds } : {}),
- },
- aggs: isComparisonSearch
- ? {
- timeseries: {
- date_histogram: {
- field: '@timestamp',
- fixed_interval: intervalString,
- min_doc_count: 0,
- extended_bounds: { min: start, max: end },
- },
- aggs: subAggs,
- },
- }
- : subAggs,
- },
- };
+ const aggs = {
+ [SERVICE_NODE_NAME]: {
+ terms: {
+ field: SERVICE_NODE_NAME,
+ missing: SERVICE_NODE_NAME_MISSING,
+ ...(size ? { size } : {}),
+ ...(isComparisonSearch ? { include: serviceNodeIds } : {}),
+ },
+ aggs: isComparisonSearch
+ ? {
+ timeseries: {
+ date_histogram: {
+ field: '@timestamp',
+ fixed_interval: intervalString,
+ min_doc_count: 0,
+ extended_bounds: { min: start, max: end },
+ },
+ aggs: subAggs,
+ },
+ }
+ : subAggs,
+ },
+ };
- const response = await apmEventClient.search({
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ],
- },
- body: { size: 0, query, aggs },
- });
+ const response = await apmEventClient.search(
+ 'get_service_instances_transaction_statistics',
+ {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ],
+ },
+ body: { size: 0, query, aggs },
+ }
+ );
- const bucketSizeInMinutes = bucketSize / 60;
+ const bucketSizeInMinutes = bucketSize / 60;
- return (
- (response.aggregations?.[SERVICE_NODE_NAME].buckets.map(
- (serviceNodeBucket) => {
- const { doc_count: count, key } = serviceNodeBucket;
- const serviceNodeName = String(key);
+ return (
+ (response.aggregations?.[SERVICE_NODE_NAME].buckets.map(
+ (serviceNodeBucket) => {
+ const { doc_count: count, key } = serviceNodeBucket;
+ const serviceNodeName = String(key);
- // Timeseries is returned when isComparisonSearch is true
- if ('timeseries' in serviceNodeBucket) {
- const { timeseries } = serviceNodeBucket;
- return {
- serviceNodeName,
- errorRate: timeseries.buckets.map((dateBucket) => ({
- x: dateBucket.key,
- y: dateBucket.failures.doc_count / dateBucket.doc_count,
- })),
- throughput: timeseries.buckets.map((dateBucket) => ({
- x: dateBucket.key,
- y: dateBucket.doc_count / bucketSizeInMinutes,
- })),
- latency: timeseries.buckets.map((dateBucket) => ({
- x: dateBucket.key,
- y: getLatencyValue({
- aggregation: dateBucket.latency,
- latencyAggregationType,
- }),
- })),
- };
- } else {
- const { failures, latency } = serviceNodeBucket;
- return {
- serviceNodeName,
- errorRate: failures.doc_count / count,
- latency: getLatencyValue({
- aggregation: latency,
- latencyAggregationType,
- }),
- throughput: calculateThroughput({ start, end, value: count }),
- };
- }
- }
- ) as Array>) || []
- );
- }
+ // Timeseries is returned when isComparisonSearch is true
+ if ('timeseries' in serviceNodeBucket) {
+ const { timeseries } = serviceNodeBucket;
+ return {
+ serviceNodeName,
+ errorRate: timeseries.buckets.map((dateBucket) => ({
+ x: dateBucket.key,
+ y: dateBucket.failures.doc_count / dateBucket.doc_count,
+ })),
+ throughput: timeseries.buckets.map((dateBucket) => ({
+ x: dateBucket.key,
+ y: dateBucket.doc_count / bucketSizeInMinutes,
+ })),
+ latency: timeseries.buckets.map((dateBucket) => ({
+ x: dateBucket.key,
+ y: getLatencyValue({
+ aggregation: dateBucket.latency,
+ latencyAggregationType,
+ }),
+ })),
+ };
+ } else {
+ const { failures, latency } = serviceNodeBucket;
+ return {
+ serviceNodeName,
+ errorRate: failures.doc_count / count,
+ latency: getLatencyValue({
+ aggregation: latency,
+ latencyAggregationType,
+ }),
+ throughput: calculateThroughput({ start, end, value: count }),
+ };
+ }
+ }
+ ) as Array>) || []
);
}
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts b/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts
index e2341b306a878..910725b005411 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts
@@ -25,7 +25,6 @@ import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'
import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { should } from './get_service_metadata_icons';
-import { withApmSpan } from '../../utils/with_apm_span';
type ServiceMetadataDetailsRaw = Pick<
TransactionRaw,
@@ -59,7 +58,7 @@ export interface ServiceMetadataDetails {
};
}
-export function getServiceMetadataDetails({
+export async function getServiceMetadataDetails({
serviceName,
setup,
searchAggregatedTransactions,
@@ -68,105 +67,106 @@ export function getServiceMetadataDetails({
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
}): Promise {
- return withApmSpan('get_service_metadata_details', async () => {
- const { start, end, apmEventClient } = setup;
+ const { start, end, apmEventClient } = setup;
- const filter = [
- { term: { [SERVICE_NAME]: serviceName } },
- ...rangeQuery(start, end),
- ];
+ const filter = [
+ { term: { [SERVICE_NAME]: serviceName } },
+ ...rangeQuery(start, end),
+ ];
- const params = {
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ProcessorEvent.error,
- ProcessorEvent.metric,
- ],
- },
- body: {
- size: 1,
- _source: [SERVICE, AGENT, HOST, CONTAINER_ID, KUBERNETES, CLOUD],
- query: { bool: { filter, should } },
- aggs: {
- serviceVersions: {
- terms: {
- field: SERVICE_VERSION,
- size: 10,
- order: { _key: 'desc' as const },
- },
+ const params = {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ProcessorEvent.error,
+ ProcessorEvent.metric,
+ ],
+ },
+ body: {
+ size: 1,
+ _source: [SERVICE, AGENT, HOST, CONTAINER_ID, KUBERNETES, CLOUD],
+ query: { bool: { filter, should } },
+ aggs: {
+ serviceVersions: {
+ terms: {
+ field: SERVICE_VERSION,
+ size: 10,
+ order: { _key: 'desc' as const },
},
- availabilityZones: {
- terms: {
- field: CLOUD_AVAILABILITY_ZONE,
- size: 10,
- },
+ },
+ availabilityZones: {
+ terms: {
+ field: CLOUD_AVAILABILITY_ZONE,
+ size: 10,
},
- machineTypes: {
- terms: {
- field: CLOUD_MACHINE_TYPE,
- size: 10,
- },
+ },
+ machineTypes: {
+ terms: {
+ field: CLOUD_MACHINE_TYPE,
+ size: 10,
},
- totalNumberInstances: { cardinality: { field: SERVICE_NODE_NAME } },
},
+ totalNumberInstances: { cardinality: { field: SERVICE_NODE_NAME } },
},
- };
-
- const response = await apmEventClient.search(params);
-
- if (response.hits.total.value === 0) {
- return {
- service: undefined,
- container: undefined,
- cloud: undefined,
- };
- }
+ },
+ };
- const { service, agent, host, kubernetes, container, cloud } = response.hits
- .hits[0]._source as ServiceMetadataDetailsRaw;
+ const response = await apmEventClient.search(
+ 'get_service_metadata_details',
+ params
+ );
- const serviceMetadataDetails = {
- versions: response.aggregations?.serviceVersions.buckets.map(
- (bucket) => bucket.key as string
- ),
- runtime: service.runtime,
- framework: service.framework?.name,
- agent,
+ if (response.hits.total.value === 0) {
+ return {
+ service: undefined,
+ container: undefined,
+ cloud: undefined,
};
+ }
+
+ const { service, agent, host, kubernetes, container, cloud } = response.hits
+ .hits[0]._source as ServiceMetadataDetailsRaw;
- const totalNumberInstances =
- response.aggregations?.totalNumberInstances.value;
+ const serviceMetadataDetails = {
+ versions: response.aggregations?.serviceVersions.buckets.map(
+ (bucket) => bucket.key as string
+ ),
+ runtime: service.runtime,
+ framework: service.framework?.name,
+ agent,
+ };
- const containerDetails =
- host || container || totalNumberInstances || kubernetes
- ? {
- os: host?.os?.platform,
- type: (!!kubernetes ? 'Kubernetes' : 'Docker') as ContainerType,
- isContainerized: !!container?.id,
- totalNumberInstances,
- }
- : undefined;
+ const totalNumberInstances =
+ response.aggregations?.totalNumberInstances.value;
- const cloudDetails = cloud
+ const containerDetails =
+ host || container || totalNumberInstances || kubernetes
? {
- provider: cloud.provider,
- projectName: cloud.project?.name,
- availabilityZones: response.aggregations?.availabilityZones.buckets.map(
- (bucket) => bucket.key as string
- ),
- machineTypes: response.aggregations?.machineTypes.buckets.map(
- (bucket) => bucket.key as string
- ),
+ os: host?.os?.platform,
+ type: (!!kubernetes ? 'Kubernetes' : 'Docker') as ContainerType,
+ isContainerized: !!container?.id,
+ totalNumberInstances,
}
: undefined;
- return {
- service: serviceMetadataDetails,
- container: containerDetails,
- cloud: cloudDetails,
- };
- });
+ const cloudDetails = cloud
+ ? {
+ provider: cloud.provider,
+ projectName: cloud.project?.name,
+ availabilityZones: response.aggregations?.availabilityZones.buckets.map(
+ (bucket) => bucket.key as string
+ ),
+ machineTypes: response.aggregations?.machineTypes.buckets.map(
+ (bucket) => bucket.key as string
+ ),
+ }
+ : undefined;
+
+ return {
+ service: serviceMetadataDetails,
+ container: containerDetails,
+ cloud: cloudDetails,
+ };
}
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts b/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts
index 94da6545c5e90..469c788a6cf17 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts
@@ -20,7 +20,6 @@ import { rangeQuery } from '../../../server/utils/queries';
import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw';
import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
-import { withApmSpan } from '../../utils/with_apm_span';
type ServiceMetadataIconsRaw = Pick<
TransactionRaw,
@@ -41,7 +40,7 @@ export const should = [
{ exists: { field: AGENT_NAME } },
];
-export function getServiceMetadataIcons({
+export async function getServiceMetadataIcons({
serviceName,
setup,
searchAggregatedTransactions,
@@ -50,55 +49,56 @@ export function getServiceMetadataIcons({
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
}): Promise {
- return withApmSpan('get_service_metadata_icons', async () => {
- const { start, end, apmEventClient } = setup;
+ const { start, end, apmEventClient } = setup;
- const filter = [
- { term: { [SERVICE_NAME]: serviceName } },
- ...rangeQuery(start, end),
- ];
+ const filter = [
+ { term: { [SERVICE_NAME]: serviceName } },
+ ...rangeQuery(start, end),
+ ];
- const params = {
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ProcessorEvent.error,
- ProcessorEvent.metric,
- ],
- },
- body: {
- size: 1,
- _source: [KUBERNETES, CLOUD_PROVIDER, CONTAINER_ID, AGENT_NAME],
- query: { bool: { filter, should } },
- },
- };
+ const params = {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ProcessorEvent.error,
+ ProcessorEvent.metric,
+ ],
+ },
+ body: {
+ size: 1,
+ _source: [KUBERNETES, CLOUD_PROVIDER, CONTAINER_ID, AGENT_NAME],
+ query: { bool: { filter, should } },
+ },
+ };
- const response = await apmEventClient.search(params);
+ const response = await apmEventClient.search(
+ 'get_service_metadata_icons',
+ params
+ );
- if (response.hits.total.value === 0) {
- return {
- agentName: undefined,
- containerType: undefined,
- cloudProvider: undefined,
- };
- }
+ if (response.hits.total.value === 0) {
+ return {
+ agentName: undefined,
+ containerType: undefined,
+ cloudProvider: undefined,
+ };
+ }
- const { kubernetes, cloud, container, agent } = response.hits.hits[0]
- ._source as ServiceMetadataIconsRaw;
+ const { kubernetes, cloud, container, agent } = response.hits.hits[0]
+ ._source as ServiceMetadataIconsRaw;
- let containerType: ContainerType;
- if (!!kubernetes) {
- containerType = 'Kubernetes';
- } else if (!!container) {
- containerType = 'Docker';
- }
+ let containerType: ContainerType;
+ if (!!kubernetes) {
+ containerType = 'Kubernetes';
+ } else if (!!container) {
+ containerType = 'Docker';
+ }
- return {
- agentName: agent?.name,
- containerType,
- cloudProvider: cloud?.provider,
- };
- });
+ return {
+ agentName: agent?.name,
+ containerType,
+ cloudProvider: cloud?.provider,
+ };
}
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 8eaf9e96c7fd9..0f0c174179052 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
@@ -13,9 +13,8 @@ import {
import { NOT_AVAILABLE_LABEL } from '../../../common/i18n';
import { mergeProjection } from '../../projections/util/merge_projection';
import { getServiceNodesProjection } from '../../projections/service_nodes';
-import { withApmSpan } from '../../utils/with_apm_span';
-export function getServiceNodeMetadata({
+export async function getServiceNodeMetadata({
kuery,
serviceName,
serviceNodeName,
@@ -26,44 +25,44 @@ export function getServiceNodeMetadata({
serviceNodeName: string;
setup: Setup & SetupTimeRange;
}) {
- return withApmSpan('get_service_node_metadata', async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const query = mergeProjection(
- getServiceNodesProjection({
- kuery,
- setup,
- serviceName,
- serviceNodeName,
- }),
- {
- body: {
- size: 0,
- aggs: {
- host: {
- terms: {
- field: HOST_NAME,
- size: 1,
- },
+ const query = mergeProjection(
+ getServiceNodesProjection({
+ kuery,
+ setup,
+ serviceName,
+ serviceNodeName,
+ }),
+ {
+ body: {
+ size: 0,
+ aggs: {
+ host: {
+ terms: {
+ field: HOST_NAME,
+ size: 1,
},
- containerId: {
- terms: {
- field: CONTAINER_ID,
- size: 1,
- },
+ },
+ containerId: {
+ terms: {
+ field: CONTAINER_ID,
+ size: 1,
},
},
},
- }
- );
+ },
+ }
+ );
- const response = await apmEventClient.search(query);
+ const response = await apmEventClient.search(
+ 'get_service_node_metadata',
+ query
+ );
- return {
- host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL,
- containerId:
- response.aggregations?.containerId.buckets[0]?.key ||
- NOT_AVAILABLE_LABEL,
- };
- });
+ return {
+ host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL,
+ containerId:
+ response.aggregations?.containerId.buckets[0]?.key || NOT_AVAILABLE_LABEL,
+ };
}
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts
index f14dba69bf404..36d372e322cbc 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts
@@ -21,7 +21,6 @@ import {
kqlQuery,
} from '../../../server/utils/queries';
import { Coordinate } from '../../../typings/timeseries';
-import { withApmSpan } from '../../utils/with_apm_span';
import {
getDocumentTypeFilterForAggregatedTransactions,
getProcessorEventForAggregatedTransactions,
@@ -68,74 +67,72 @@ export async function getServiceTransactionGroupDetailedStatistics({
impact: number;
}>
> {
- return withApmSpan(
- 'get_service_transaction_group_detailed_statistics',
- async () => {
- const { apmEventClient } = setup;
- const { intervalString } = getBucketSize({ start, end, numBuckets });
+ const { apmEventClient } = setup;
+ const { intervalString } = getBucketSize({ start, end, numBuckets });
- const field = getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- );
+ const field = getTransactionDurationFieldForAggregatedTransactions(
+ searchAggregatedTransactions
+ );
- const response = await apmEventClient.search({
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ],
+ const response = await apmEventClient.search(
+ 'get_service_transaction_group_detailed_statistics',
+ {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { term: { [SERVICE_NAME]: serviceName } },
+ { term: { [TRANSACTION_TYPE]: transactionType } },
+ ...getDocumentTypeFilterForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ ],
+ },
},
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- { term: { [SERVICE_NAME]: serviceName } },
- { term: { [TRANSACTION_TYPE]: transactionType } },
- ...getDocumentTypeFilterForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ...kqlQuery(kuery),
- ],
+ aggs: {
+ total_duration: { sum: { field } },
+ transaction_groups: {
+ terms: {
+ field: TRANSACTION_NAME,
+ include: transactionNames,
+ size: transactionNames.length,
},
- },
- aggs: {
- total_duration: { sum: { field } },
- transaction_groups: {
- terms: {
- field: TRANSACTION_NAME,
- include: transactionNames,
- size: transactionNames.length,
+ aggs: {
+ transaction_group_total_duration: {
+ sum: { field },
},
- aggs: {
- transaction_group_total_duration: {
- sum: { field },
+ timeseries: {
+ date_histogram: {
+ field: '@timestamp',
+ fixed_interval: intervalString,
+ min_doc_count: 0,
+ extended_bounds: {
+ min: start,
+ max: end,
+ },
},
- timeseries: {
- date_histogram: {
- field: '@timestamp',
- fixed_interval: intervalString,
- min_doc_count: 0,
- extended_bounds: {
- min: start,
- max: end,
+ aggs: {
+ throughput_rate: {
+ rate: {
+ unit: 'minute',
},
},
- aggs: {
- throughput_rate: {
- rate: {
- unit: 'minute',
- },
- },
- ...getLatencyAggregation(latencyAggregationType, field),
- [EVENT_OUTCOME]: {
- terms: {
- field: EVENT_OUTCOME,
- include: [EventOutcome.failure, EventOutcome.success],
- },
+ ...getLatencyAggregation(latencyAggregationType, field),
+ [EVENT_OUTCOME]: {
+ terms: {
+ field: EVENT_OUTCOME,
+ include: [EventOutcome.failure, EventOutcome.success],
},
},
},
@@ -143,46 +140,42 @@ export async function getServiceTransactionGroupDetailedStatistics({
},
},
},
- });
-
- const buckets = response.aggregations?.transaction_groups.buckets ?? [];
-
- const totalDuration = response.aggregations?.total_duration.value;
- return buckets.map((bucket) => {
- const transactionName = bucket.key as string;
- const latency = bucket.timeseries.buckets.map((timeseriesBucket) => ({
- x: timeseriesBucket.key,
- y: getLatencyValue({
- latencyAggregationType,
- aggregation: timeseriesBucket.latency,
- }),
- }));
- const throughput = bucket.timeseries.buckets.map(
- (timeseriesBucket) => ({
- x: timeseriesBucket.key,
- y: timeseriesBucket.throughput_rate.value,
- })
- );
- const errorRate = bucket.timeseries.buckets.map((timeseriesBucket) => ({
- x: timeseriesBucket.key,
- y: calculateTransactionErrorPercentage(
- timeseriesBucket[EVENT_OUTCOME]
- ),
- }));
- const transactionGroupTotalDuration =
- bucket.transaction_group_total_duration.value || 0;
- return {
- transactionName,
- latency,
- throughput,
- errorRate,
- impact: totalDuration
- ? (transactionGroupTotalDuration * 100) / totalDuration
- : 0,
- };
- });
+ },
}
);
+
+ const buckets = response.aggregations?.transaction_groups.buckets ?? [];
+
+ const totalDuration = response.aggregations?.total_duration.value;
+ return buckets.map((bucket) => {
+ const transactionName = bucket.key as string;
+ const latency = bucket.timeseries.buckets.map((timeseriesBucket) => ({
+ x: timeseriesBucket.key,
+ y: getLatencyValue({
+ latencyAggregationType,
+ aggregation: timeseriesBucket.latency,
+ }),
+ }));
+ const throughput = bucket.timeseries.buckets.map((timeseriesBucket) => ({
+ x: timeseriesBucket.key,
+ y: timeseriesBucket.throughput_rate.value,
+ }));
+ const errorRate = bucket.timeseries.buckets.map((timeseriesBucket) => ({
+ x: timeseriesBucket.key,
+ y: calculateTransactionErrorPercentage(timeseriesBucket[EVENT_OUTCOME]),
+ }));
+ const transactionGroupTotalDuration =
+ bucket.transaction_group_total_duration.value || 0;
+ return {
+ transactionName,
+ latency,
+ throughput,
+ errorRate,
+ impact: totalDuration
+ ? (transactionGroupTotalDuration * 100) / totalDuration
+ : 0,
+ };
+ });
}
export async function getServiceTransactionGroupDetailedStatisticsPeriods({
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts
index 28574bab4df21..a4cc27c875d73 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts
@@ -18,7 +18,6 @@ import {
rangeQuery,
kqlQuery,
} from '../../../server/utils/queries';
-import { withApmSpan } from '../../utils/with_apm_span';
import {
getDocumentTypeFilterForAggregatedTransactions,
getProcessorEventForAggregatedTransactions,
@@ -56,14 +55,15 @@ export async function getServiceTransactionGroups({
transactionType: string;
latencyAggregationType: LatencyAggregationType;
}) {
- return withApmSpan('get_service_transaction_groups', async () => {
- const { apmEventClient, start, end } = setup;
+ const { apmEventClient, start, end } = setup;
- const field = getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- );
+ const field = getTransactionDurationFieldForAggregatedTransactions(
+ searchAggregatedTransactions
+ );
- const response = await apmEventClient.search({
+ const response = await apmEventClient.search(
+ 'get_service_transaction_groups',
+ {
apm: {
events: [
getProcessorEventForAggregatedTransactions(
@@ -110,45 +110,45 @@ export async function getServiceTransactionGroups({
},
},
},
- });
+ }
+ );
- const totalDuration = response.aggregations?.total_duration.value;
+ const totalDuration = response.aggregations?.total_duration.value;
- const transactionGroups =
- response.aggregations?.transaction_groups.buckets.map((bucket) => {
- const errorRate = calculateTransactionErrorPercentage(
- bucket[EVENT_OUTCOME]
- );
+ const transactionGroups =
+ response.aggregations?.transaction_groups.buckets.map((bucket) => {
+ const errorRate = calculateTransactionErrorPercentage(
+ bucket[EVENT_OUTCOME]
+ );
- const transactionGroupTotalDuration =
- bucket.transaction_group_total_duration.value || 0;
+ const transactionGroupTotalDuration =
+ bucket.transaction_group_total_duration.value || 0;
- return {
- name: bucket.key as string,
- latency: getLatencyValue({
- latencyAggregationType,
- aggregation: bucket.latency,
- }),
- throughput: calculateThroughput({
- start,
- end,
- value: bucket.doc_count,
- }),
- errorRate,
- impact: totalDuration
- ? (transactionGroupTotalDuration * 100) / totalDuration
- : 0,
- };
- }) ?? [];
+ return {
+ name: bucket.key as string,
+ latency: getLatencyValue({
+ latencyAggregationType,
+ aggregation: bucket.latency,
+ }),
+ throughput: calculateThroughput({
+ start,
+ end,
+ value: bucket.doc_count,
+ }),
+ errorRate,
+ impact: totalDuration
+ ? (transactionGroupTotalDuration * 100) / totalDuration
+ : 0,
+ };
+ }) ?? [];
- return {
- transactionGroups: transactionGroups.map((transactionGroup) => ({
- ...transactionGroup,
- transactionType,
- })),
- isAggregationAccurate:
- (response.aggregations?.transaction_groups.sum_other_doc_count ?? 0) ===
- 0,
- };
- });
+ return {
+ transactionGroups: transactionGroups.map((transactionGroup) => ({
+ ...transactionGroup,
+ transactionType,
+ })),
+ isAggregationAccurate:
+ (response.aggregations?.transaction_groups.sum_other_doc_count ?? 0) ===
+ 0,
+ };
}
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 e280ab6db1665..f38a7fba09d96 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
@@ -15,9 +15,8 @@ import {
getDocumentTypeFilterForAggregatedTransactions,
getProcessorEventForAggregatedTransactions,
} from '../helpers/aggregated_transactions';
-import { withApmSpan } from '../../utils/with_apm_span';
-export function getServiceTransactionTypes({
+export async function getServiceTransactionTypes({
setup,
serviceName,
searchAggregatedTransactions,
@@ -26,41 +25,42 @@ export function getServiceTransactionTypes({
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
}) {
- return withApmSpan('get_service_transaction_types', async () => {
- const { start, end, apmEventClient } = setup;
+ const { start, end, apmEventClient } = setup;
- const params = {
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- ...getDocumentTypeFilterForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- { term: { [SERVICE_NAME]: serviceName } },
- ...rangeQuery(start, end),
- ],
- },
+ const params = {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ ...getDocumentTypeFilterForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ { term: { [SERVICE_NAME]: serviceName } },
+ ...rangeQuery(start, end),
+ ],
},
- aggs: {
- types: {
- terms: { field: TRANSACTION_TYPE, size: 100 },
- },
+ },
+ aggs: {
+ types: {
+ terms: { field: TRANSACTION_TYPE, size: 100 },
},
},
- };
+ },
+ };
- const { aggregations } = await apmEventClient.search(params);
- const transactionTypes =
- aggregations?.types.buckets.map((bucket) => bucket.key as string) || [];
- return { transactionTypes };
- });
+ const { aggregations } = await apmEventClient.search(
+ 'get_service_transaction_types',
+ 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 b42fd340bfb42..f33bedb6ef4fb 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
@@ -9,35 +9,31 @@ import { rangeQuery } from '../../../../server/utils/queries';
import { ProcessorEvent } from '../../../../common/processor_event';
import { OBSERVER_VERSION_MAJOR } from '../../../../common/elasticsearch_fieldnames';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
-import { withApmSpan } from '../../../utils/with_apm_span';
// returns true if 6.x data is found
export async function getLegacyDataStatus(setup: Setup & SetupTimeRange) {
- return withApmSpan('get_legacy_data_status', async () => {
- const { apmEventClient, start, end } = setup;
+ const { apmEventClient, start, end } = setup;
- const params = {
- terminateAfter: 1,
- apm: {
- events: [ProcessorEvent.transaction],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- { range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } },
- ...rangeQuery(start, end),
- ],
- },
+ const params = {
+ terminateAfter: 1,
+ apm: {
+ events: [ProcessorEvent.transaction],
+ includeLegacyData: true,
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } },
+ ...rangeQuery(start, end),
+ ],
},
},
- };
+ },
+ };
- const resp = await apmEventClient.search(params, {
- includeLegacyData: true,
- });
- const hasLegacyData = resp.hits.total.value > 0;
- return hasLegacyData;
- });
+ const resp = await apmEventClient.search('get_legacy_data_status', params);
+ const hasLegacyData = resp.hits.total.value > 0;
+ return hasLegacyData;
}
diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts
index 1e36df379e964..019ab8770887a 100644
--- a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts
@@ -33,7 +33,6 @@ import {
getOutcomeAggregation,
} from '../../helpers/transaction_error_rate';
import { ServicesItemsSetup } from './get_services_items';
-import { withApmSpan } from '../../../utils/with_apm_span';
interface AggregationParams {
environment?: string;
@@ -50,23 +49,24 @@ export async function getServiceTransactionStats({
searchAggregatedTransactions,
maxNumServices,
}: AggregationParams) {
- return withApmSpan('get_service_transaction_stats', async () => {
- const { apmEventClient, start, end } = setup;
+ const { apmEventClient, start, end } = setup;
- const outcomes = getOutcomeAggregation();
+ const outcomes = getOutcomeAggregation();
- const metrics = {
- avg_duration: {
- avg: {
- field: getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- },
+ const metrics = {
+ avg_duration: {
+ avg: {
+ field: getTransactionDurationFieldForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
},
- outcomes,
- };
+ },
+ outcomes,
+ };
- const response = await apmEventClient.search({
+ const response = await apmEventClient.search(
+ 'get_service_transaction_stats',
+ {
apm: {
events: [
getProcessorEventForAggregatedTransactions(
@@ -133,64 +133,64 @@ export async function getServiceTransactionStats({
},
},
},
- });
+ }
+ );
- return (
- response.aggregations?.services.buckets.map((bucket) => {
- const topTransactionTypeBucket =
- bucket.transactionType.buckets.find(
- ({ key }) =>
- key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD
- ) ?? bucket.transactionType.buckets[0];
+ return (
+ response.aggregations?.services.buckets.map((bucket) => {
+ const topTransactionTypeBucket =
+ bucket.transactionType.buckets.find(
+ ({ key }) =>
+ key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD
+ ) ?? bucket.transactionType.buckets[0];
- return {
- serviceName: bucket.key as string,
- transactionType: topTransactionTypeBucket.key as string,
- environments: topTransactionTypeBucket.environments.buckets.map(
- (environmentBucket) => environmentBucket.key as string
+ return {
+ serviceName: bucket.key as string,
+ transactionType: topTransactionTypeBucket.key as string,
+ environments: topTransactionTypeBucket.environments.buckets.map(
+ (environmentBucket) => environmentBucket.key as string
+ ),
+ agentName: topTransactionTypeBucket.sample.top[0].metrics[
+ AGENT_NAME
+ ] as AgentName,
+ avgResponseTime: {
+ value: topTransactionTypeBucket.avg_duration.value,
+ timeseries: topTransactionTypeBucket.timeseries.buckets.map(
+ (dateBucket) => ({
+ x: dateBucket.key,
+ y: dateBucket.avg_duration.value,
+ })
),
- agentName: topTransactionTypeBucket.sample.top[0].metrics[
- AGENT_NAME
- ] as AgentName,
- avgResponseTime: {
- value: topTransactionTypeBucket.avg_duration.value,
- timeseries: topTransactionTypeBucket.timeseries.buckets.map(
- (dateBucket) => ({
- x: dateBucket.key,
- y: dateBucket.avg_duration.value,
- })
- ),
- },
- transactionErrorRate: {
- value: calculateTransactionErrorPercentage(
- topTransactionTypeBucket.outcomes
- ),
- timeseries: topTransactionTypeBucket.timeseries.buckets.map(
- (dateBucket) => ({
- x: dateBucket.key,
- y: calculateTransactionErrorPercentage(dateBucket.outcomes),
- })
- ),
- },
- transactionsPerMinute: {
- value: calculateThroughput({
- start,
- end,
- value: topTransactionTypeBucket.doc_count,
- }),
- timeseries: topTransactionTypeBucket.timeseries.buckets.map(
- (dateBucket) => ({
- x: dateBucket.key,
- y: calculateThroughput({
- start,
- end,
- value: dateBucket.doc_count,
- }),
- })
- ),
- },
- };
- }) ?? []
- );
- });
+ },
+ transactionErrorRate: {
+ value: calculateTransactionErrorPercentage(
+ topTransactionTypeBucket.outcomes
+ ),
+ timeseries: topTransactionTypeBucket.timeseries.buckets.map(
+ (dateBucket) => ({
+ x: dateBucket.key,
+ y: calculateTransactionErrorPercentage(dateBucket.outcomes),
+ })
+ ),
+ },
+ transactionsPerMinute: {
+ value: calculateThroughput({
+ start,
+ end,
+ value: topTransactionTypeBucket.doc_count,
+ }),
+ timeseries: topTransactionTypeBucket.timeseries.buckets.map(
+ (dateBucket) => ({
+ x: dateBucket.key,
+ y: calculateThroughput({
+ start,
+ end,
+ value: dateBucket.doc_count,
+ }),
+ })
+ ),
+ },
+ };
+ }) ?? []
+ );
}
diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts
index 906cc62e64d1a..4692d1122b16c 100644
--- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts
@@ -14,9 +14,8 @@ import {
import { environmentQuery, kqlQuery, rangeQuery } from '../../../utils/queries';
import { ProcessorEvent } from '../../../../common/processor_event';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
-import { withApmSpan } from '../../../utils/with_apm_span';
-export function getServicesFromMetricDocuments({
+export async function getServicesFromMetricDocuments({
environment,
setup,
maxNumServices,
@@ -27,10 +26,11 @@ export function getServicesFromMetricDocuments({
maxNumServices: number;
kuery?: string;
}) {
- return withApmSpan('get_services_from_metric_documents', async () => {
- const { apmEventClient, start, end } = setup;
+ const { apmEventClient, start, end } = setup;
- const response = await apmEventClient.search({
+ const response = await apmEventClient.search(
+ 'get_services_from_metric_documents',
+ {
apm: {
events: [ProcessorEvent.metric],
},
@@ -67,18 +67,18 @@ export function getServicesFromMetricDocuments({
},
},
},
- });
+ }
+ );
- return (
- response.aggregations?.services.buckets.map((bucket) => {
- return {
- serviceName: bucket.key as string,
- environments: bucket.environments.buckets.map(
- (envBucket) => envBucket.key as string
- ),
- agentName: bucket.latest.top[0].metrics[AGENT_NAME] as AgentName,
- };
- }) ?? []
- );
- });
+ return (
+ response.aggregations?.services.buckets.map((bucket) => {
+ return {
+ serviceName: bucket.key as string,
+ environments: bucket.environments.buckets.map(
+ (envBucket) => envBucket.key as string
+ ),
+ agentName: bucket.latest.top[0].metrics[AGENT_NAME] as AgentName,
+ };
+ }) ?? []
+ );
}
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 28f6944fd24da..97b8a8fa5505b 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
@@ -6,29 +6,26 @@
*/
import { ProcessorEvent } from '../../../../common/processor_event';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { Setup } from '../../helpers/setup_request';
// Note: this logic is duplicated in tutorials/apm/envs/on_prem
export async function hasHistoricalAgentData(setup: Setup) {
- return withApmSpan('has_historical_agent_data', async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const params = {
- terminateAfter: 1,
- apm: {
- events: [
- ProcessorEvent.error,
- ProcessorEvent.metric,
- ProcessorEvent.transaction,
- ],
- },
- body: {
- size: 0,
- },
- };
+ const params = {
+ terminateAfter: 1,
+ apm: {
+ events: [
+ ProcessorEvent.error,
+ ProcessorEvent.metric,
+ ProcessorEvent.transaction,
+ ],
+ },
+ body: {
+ size: 0,
+ },
+ };
- const resp = await apmEventClient.search(params);
- return resp.hits.total.value > 0;
- });
+ const resp = await apmEventClient.search('has_historical_agent_data', params);
+ return resp.hits.total.value > 0;
}
diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts
index 5f5008a28c232..b0cb917d302fc 100644
--- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts
@@ -21,7 +21,6 @@ import {
} from '../helpers/aggregated_transactions';
import { getBucketSize } from '../helpers/get_bucket_size';
import { Setup } from '../helpers/setup_request';
-import { withApmSpan } from '../../utils/with_apm_span';
interface Options {
environment?: string;
@@ -88,20 +87,18 @@ function fetcher({
},
};
- return apmEventClient.search(params);
+ return apmEventClient.search('get_throughput_for_service', params);
}
-export function getThroughput(options: Options) {
- return withApmSpan('get_throughput_for_service', async () => {
- const response = await fetcher(options);
+export async function getThroughput(options: Options) {
+ const response = await fetcher(options);
- return (
- response.aggregations?.timeseries.buckets.map((bucket) => {
- return {
- x: bucket.key,
- y: bucket.throughput.value,
- };
- }) ?? []
- );
- });
+ return (
+ response.aggregations?.timeseries.buckets.map((bucket) => {
+ return {
+ x: bucket.key,
+ y: bucket.throughput.value,
+ };
+ }) ?? []
+ );
}
diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts
index 858f36e6e2c13..bb98abf724db4 100644
--- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts
+++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts
@@ -41,7 +41,7 @@ const maybeAdd = (to: any[], value: any) => {
to.push(value);
};
-function getProfilingStats({
+async function getProfilingStats({
apmEventClient,
filter,
valueTypeField,
@@ -50,49 +50,47 @@ function getProfilingStats({
filter: ESFilter[];
valueTypeField: string;
}) {
- return withApmSpan('get_profiling_stats', async () => {
- const response = await apmEventClient.search({
- apm: {
- events: [ProcessorEvent.profile],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter,
- },
+ const response = await apmEventClient.search('get_profiling_stats', {
+ apm: {
+ events: [ProcessorEvent.profile],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter,
},
- aggs: {
- stacks: {
- terms: {
- field: PROFILE_TOP_ID,
- size: MAX_STACK_IDS,
- order: {
- value: 'desc',
- },
+ },
+ aggs: {
+ stacks: {
+ terms: {
+ field: PROFILE_TOP_ID,
+ size: MAX_STACK_IDS,
+ order: {
+ value: 'desc',
},
- aggs: {
- value: {
- sum: {
- field: valueTypeField,
- },
+ },
+ aggs: {
+ value: {
+ sum: {
+ field: valueTypeField,
},
},
},
},
},
- });
+ },
+ });
- const stacks =
- response.aggregations?.stacks.buckets.map((stack) => {
- return {
- id: stack.key as string,
- value: stack.value.value!,
- };
- }) ?? [];
+ const stacks =
+ response.aggregations?.stacks.buckets.map((stack) => {
+ return {
+ id: stack.key as string,
+ value: stack.value.value!,
+ };
+ }) ?? [];
- return stacks;
- });
+ return stacks;
}
function getProfilesWithStacks({
@@ -103,8 +101,9 @@ function getProfilesWithStacks({
filter: ESFilter[];
}) {
return withApmSpan('get_profiles_with_stacks', async () => {
- const cardinalityResponse = await withApmSpan('get_top_cardinality', () =>
- apmEventClient.search({
+ const cardinalityResponse = await apmEventClient.search(
+ 'get_top_cardinality',
+ {
apm: {
events: [ProcessorEvent.profile],
},
@@ -121,7 +120,7 @@ function getProfilesWithStacks({
},
},
},
- })
+ }
);
const cardinality = cardinalityResponse.aggregations?.top.value ?? 0;
@@ -140,39 +139,37 @@ function getProfilesWithStacks({
const allResponses = await withApmSpan('get_all_stacks', async () => {
return Promise.all(
[...new Array(partitions)].map(async (_, num) => {
- const response = await withApmSpan('get_partition', () =>
- apmEventClient.search({
- apm: {
- events: [ProcessorEvent.profile],
- },
- body: {
- query: {
- bool: {
- filter,
- },
+ const response = await apmEventClient.search('get_partition', {
+ apm: {
+ events: [ProcessorEvent.profile],
+ },
+ body: {
+ query: {
+ bool: {
+ filter,
},
- aggs: {
- top: {
- terms: {
- field: PROFILE_TOP_ID,
- size: Math.max(MAX_STACKS_PER_REQUEST),
- include: {
- num_partitions: partitions,
- partition: num,
- },
+ },
+ aggs: {
+ top: {
+ terms: {
+ field: PROFILE_TOP_ID,
+ size: Math.max(MAX_STACKS_PER_REQUEST),
+ include: {
+ num_partitions: partitions,
+ partition: num,
},
- aggs: {
- latest: {
- top_hits: {
- _source: [PROFILE_TOP_ID, PROFILE_STACK],
- },
+ },
+ aggs: {
+ latest: {
+ top_hits: {
+ _source: [PROFILE_TOP_ID, PROFILE_STACK],
},
},
},
},
},
- })
- );
+ },
+ });
return (
response.aggregations?.top.buckets.flatMap((bucket) => {
diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts
index 93fa029da8c72..af3cd6596a8c1 100644
--- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts
+++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts
@@ -17,7 +17,6 @@ import {
} from '../../../../common/profiling';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
import { getBucketSize } from '../../helpers/get_bucket_size';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { kqlQuery } from '../../../utils/queries';
const configMap = mapValues(
@@ -38,10 +37,11 @@ export async function getServiceProfilingTimeline({
setup: Setup & SetupTimeRange;
environment?: string;
}) {
- return withApmSpan('get_service_profiling_timeline', async () => {
- const { apmEventClient, start, end } = setup;
+ const { apmEventClient, start, end } = setup;
- const response = await apmEventClient.search({
+ const response = await apmEventClient.search(
+ 'get_service_profiling_timeline',
+ {
apm: {
events: [ProcessorEvent.profile],
},
@@ -96,29 +96,29 @@ export async function getServiceProfilingTimeline({
},
},
},
- });
+ }
+ );
- const { aggregations } = response;
+ const { aggregations } = response;
- if (!aggregations) {
- return [];
- }
+ if (!aggregations) {
+ return [];
+ }
- return aggregations.timeseries.buckets.map((bucket) => {
- return {
- x: bucket.key,
- valueTypes: {
- unknown: bucket.value_type.buckets.unknown.num_profiles.value,
- // TODO: use enum as object key. not possible right now
- // because of https://github.com/microsoft/TypeScript/issues/37888
- ...mapValues(configMap, (_, key) => {
- return (
- bucket.value_type.buckets[key as ProfilingValueType]?.num_profiles
- .value ?? 0
- );
- }),
- },
- };
- });
+ return aggregations.timeseries.buckets.map((bucket) => {
+ return {
+ x: bucket.key,
+ valueTypes: {
+ unknown: bucket.value_type.buckets.unknown.num_profiles.value,
+ // TODO: use enum as object key. not possible right now
+ // because of https://github.com/microsoft/TypeScript/issues/37888
+ ...mapValues(configMap, (_, key) => {
+ return (
+ bucket.value_type.buckets[key as ProfilingValueType]?.num_profiles
+ .value ?? 0
+ );
+ }),
+ },
+ };
});
}
diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts
index b167eff65ee0a..6adaca9c1a93d 100644
--- a/x-pack/plugins/apm/server/lib/services/queries.test.ts
+++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts
@@ -55,7 +55,7 @@ describe('services queries', () => {
})
);
- const allParams = mock.spy.mock.calls.map((call) => call[0]);
+ const allParams = mock.spy.mock.calls.map((call) => call[1]);
expect(allParams).toMatchSnapshot();
});
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 1885382435562..c112c3be3362b 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
@@ -12,7 +12,6 @@ import {
AgentConfigurationIntake,
} from '../../../../common/agent_configuration/configuration_types';
import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client';
-import { withApmSpan } from '../../../utils/with_apm_span';
export function createOrUpdateConfiguration({
configurationId,
@@ -23,30 +22,28 @@ export function createOrUpdateConfiguration({
configurationIntake: AgentConfigurationIntake;
setup: Setup;
}) {
- return withApmSpan('create_or_update_configuration', async () => {
- const { internalClient, indices } = setup;
+ const { internalClient, indices } = setup;
- const params: APMIndexDocumentParams = {
- refresh: true,
- index: indices.apmAgentConfigurationIndex,
- body: {
- agent_name: configurationIntake.agent_name,
- service: {
- name: configurationIntake.service.name,
- environment: configurationIntake.service.environment,
- },
- settings: configurationIntake.settings,
- '@timestamp': Date.now(),
- applied_by_agent: false,
- etag: hash(configurationIntake),
+ const params: APMIndexDocumentParams = {
+ refresh: true,
+ index: indices.apmAgentConfigurationIndex,
+ body: {
+ agent_name: configurationIntake.agent_name,
+ service: {
+ name: configurationIntake.service.name,
+ environment: configurationIntake.service.environment,
},
- };
+ settings: configurationIntake.settings,
+ '@timestamp': Date.now(),
+ applied_by_agent: false,
+ etag: hash(configurationIntake),
+ },
+ };
- // by specifying an id elasticsearch will delete the previous doc and insert the updated doc
- if (configurationId) {
- params.id = configurationId;
- }
+ // by specifying an id elasticsearch will delete the previous doc and insert the updated doc
+ if (configurationId) {
+ params.id = configurationId;
+ }
- return internalClient.index(params);
- });
+ return internalClient.index('create_or_update_agent_configuration', params);
}
diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts
index 6ed6f79979889..125c97730a6fa 100644
--- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts
+++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { withApmSpan } from '../../../utils/with_apm_span';
import { Setup } from '../../helpers/setup_request';
export async function deleteConfiguration({
@@ -15,15 +14,13 @@ export async function deleteConfiguration({
configurationId: string;
setup: Setup;
}) {
- return withApmSpan('delete_agent_configuration', async () => {
- const { internalClient, indices } = setup;
+ const { internalClient, indices } = setup;
- const params = {
- refresh: 'wait_for' as const,
- index: indices.apmAgentConfigurationIndex,
- id: configurationId,
- };
+ const params = {
+ refresh: 'wait_for' as const,
+ index: indices.apmAgentConfigurationIndex,
+ id: configurationId,
+ };
- return internalClient.delete(params);
- });
+ return internalClient.delete('delete_agent_configuration', params);
}
diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts
index 9fd4849c7640a..3543d38f7b5d1 100644
--- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts
+++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts
@@ -11,47 +11,45 @@ import {
SERVICE_ENVIRONMENT,
SERVICE_NAME,
} from '../../../../common/elasticsearch_fieldnames';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { Setup } from '../../helpers/setup_request';
import { convertConfigSettingsToString } from './convert_settings_to_string';
-export function findExactConfiguration({
+export async function findExactConfiguration({
service,
setup,
}: {
service: AgentConfiguration['service'];
setup: Setup;
}) {
- return withApmSpan('find_exact_agent_configuration', async () => {
- const { internalClient, indices } = setup;
-
- const serviceNameFilter = service.name
- ? { term: { [SERVICE_NAME]: service.name } }
- : { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } };
-
- const environmentFilter = service.environment
- ? { term: { [SERVICE_ENVIRONMENT]: service.environment } }
- : { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } };
-
- const params = {
- index: indices.apmAgentConfigurationIndex,
- body: {
- query: {
- bool: { filter: [serviceNameFilter, environmentFilter] },
- },
+ const { internalClient, indices } = setup;
+
+ const serviceNameFilter = service.name
+ ? { term: { [SERVICE_NAME]: service.name } }
+ : { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } };
+
+ const environmentFilter = service.environment
+ ? { term: { [SERVICE_ENVIRONMENT]: service.environment } }
+ : { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } };
+
+ const params = {
+ index: indices.apmAgentConfigurationIndex,
+ body: {
+ query: {
+ bool: { filter: [serviceNameFilter, environmentFilter] },
},
- };
+ },
+ };
- const resp = await internalClient.search(
- params
- );
+ const resp = await internalClient.search(
+ 'find_exact_agent_configuration',
+ params
+ );
- const hit = resp.hits.hits[0] as SearchHit | undefined;
+ const hit = resp.hits.hits[0] as SearchHit | undefined;
- if (!hit) {
- return;
- }
+ if (!hit) {
+ return;
+ }
- return convertConfigSettingsToString(hit);
- });
+ return convertConfigSettingsToString(hit);
}
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 379ed12e37389..0b6dd10b42e25 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
@@ -9,7 +9,6 @@ import { ProcessorEvent } from '../../../../common/processor_event';
import { Setup } from '../../helpers/setup_request';
import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames';
import { AGENT_NAME } from '../../../../common/elasticsearch_fieldnames';
-import { withApmSpan } from '../../../utils/with_apm_span';
export async function getAgentNameByService({
serviceName,
@@ -18,35 +17,36 @@ export async function getAgentNameByService({
serviceName: string;
setup: Setup;
}) {
- return withApmSpan('get_agent_name_by_service', async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const params = {
- terminateAfter: 1,
- apm: {
- events: [
- ProcessorEvent.transaction,
- ProcessorEvent.error,
- ProcessorEvent.metric,
- ],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter: [{ term: { [SERVICE_NAME]: serviceName } }],
- },
+ const params = {
+ terminateAfter: 1,
+ apm: {
+ events: [
+ ProcessorEvent.transaction,
+ ProcessorEvent.error,
+ ProcessorEvent.metric,
+ ],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [{ term: { [SERVICE_NAME]: serviceName } }],
},
- aggs: {
- agent_names: {
- terms: { field: AGENT_NAME, size: 1 },
- },
+ },
+ aggs: {
+ agent_names: {
+ terms: { field: AGENT_NAME, size: 1 },
},
},
- };
+ },
+ };
- const { aggregations } = await apmEventClient.search(params);
- const agentName = aggregations?.agent_names.buckets[0]?.key;
- return agentName as string | undefined;
- });
+ const { aggregations } = await apmEventClient.search(
+ 'get_agent_name_by_service',
+ 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_environments/get_existing_environments_for_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts
index 4a32b3c3a370b..124a373d3cf07 100644
--- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts
+++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { withApmSpan } from '../../../../utils/with_apm_span';
import { Setup } from '../../../helpers/setup_request';
import {
SERVICE_NAME,
@@ -20,36 +19,37 @@ export async function getExistingEnvironmentsForService({
serviceName: string | undefined;
setup: Setup;
}) {
- return withApmSpan('get_existing_environments_for_service', async () => {
- const { internalClient, indices, config } = setup;
- const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
+ const { internalClient, indices, config } = setup;
+ const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
- const bool = serviceName
- ? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] }
- : { must_not: [{ exists: { field: SERVICE_NAME } }] };
+ const bool = serviceName
+ ? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] }
+ : { must_not: [{ exists: { field: SERVICE_NAME } }] };
- const params = {
- index: indices.apmAgentConfigurationIndex,
- body: {
- size: 0,
- query: { bool },
- aggs: {
- environments: {
- terms: {
- field: SERVICE_ENVIRONMENT,
- missing: ALL_OPTION_VALUE,
- size: maxServiceEnvironments,
- },
+ const params = {
+ index: indices.apmAgentConfigurationIndex,
+ body: {
+ size: 0,
+ query: { bool },
+ aggs: {
+ environments: {
+ terms: {
+ field: SERVICE_ENVIRONMENT,
+ missing: ALL_OPTION_VALUE,
+ size: maxServiceEnvironments,
},
},
},
- };
+ },
+ };
- const resp = await internalClient.search(params);
- const existingEnvironments =
- resp.aggregations?.environments.buckets.map(
- (bucket) => bucket.key as string
- ) || [];
- return existingEnvironments;
- });
+ const resp = await internalClient.search(
+ 'get_existing_environments_for_service',
+ params
+ );
+ const existingEnvironments =
+ resp.aggregations?.environments.buckets.map(
+ (bucket) => bucket.key as string
+ ) || [];
+ return existingEnvironments;
}
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 9c56455f45902..0786bc6bc2771 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
@@ -11,52 +11,52 @@ import { PromiseReturnType } from '../../../../../observability/typings/common';
import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames';
import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration/all_option';
import { getProcessorEventForAggregatedTransactions } from '../../helpers/aggregated_transactions';
-import { withApmSpan } from '../../../utils/with_apm_span';
export type AgentConfigurationServicesAPIResponse = PromiseReturnType<
typeof getServiceNames
>;
-export function getServiceNames({
+export async function getServiceNames({
setup,
searchAggregatedTransactions,
}: {
setup: Setup;
searchAggregatedTransactions: boolean;
}) {
- return withApmSpan('get_service_names_for_agent_config', async () => {
- const { apmEventClient, config } = setup;
- const maxServiceSelection = config['xpack.apm.maxServiceSelection'];
+ const { apmEventClient, config } = setup;
+ const maxServiceSelection = config['xpack.apm.maxServiceSelection'];
- const params = {
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ProcessorEvent.error,
- ProcessorEvent.metric,
- ],
- },
- body: {
- timeout: '1ms',
- size: 0,
- aggs: {
- services: {
- terms: {
- field: SERVICE_NAME,
- size: maxServiceSelection,
- min_doc_count: 0,
- },
+ const params = {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ProcessorEvent.error,
+ ProcessorEvent.metric,
+ ],
+ },
+ body: {
+ timeout: '1ms',
+ size: 0,
+ aggs: {
+ services: {
+ terms: {
+ field: SERVICE_NAME,
+ size: maxServiceSelection,
+ min_doc_count: 0,
},
},
},
- };
+ },
+ };
- const resp = await apmEventClient.search(params);
- const serviceNames =
- resp.aggregations?.services.buckets
- .map((bucket) => bucket.key as string)
- .sort() || [];
- return [ALL_OPTION_VALUE, ...serviceNames];
- });
+ const resp = await apmEventClient.search(
+ 'get_service_names_for_agent_config',
+ params
+ );
+ const serviceNames =
+ resp.aggregations?.services.buckets
+ .map((bucket) => bucket.key as string)
+ .sort() || [];
+ return [ALL_OPTION_VALUE, ...serviceNames];
}
diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts
index adcfe88392dc8..098888c23ccbc 100644
--- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts
+++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts
@@ -8,7 +8,6 @@
import { Setup } from '../../helpers/setup_request';
import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types';
import { convertConfigSettingsToString } from './convert_settings_to_string';
-import { withApmSpan } from '../../../utils/with_apm_span';
export async function listConfigurations({ setup }: { setup: Setup }) {
const { internalClient, indices } = setup;
@@ -18,8 +17,9 @@ export async function listConfigurations({ setup }: { setup: Setup }) {
size: 200,
};
- const resp = await withApmSpan('list_agent_configurations', () =>
- internalClient.search(params)
+ const resp = await internalClient.search(
+ 'list_agent_configuration',
+ params
);
return resp.hits.hits
diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts
index 2026742a936a4..5fa4993921570 100644
--- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts
+++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts
@@ -29,5 +29,8 @@ export async function markAppliedByAgent({
},
};
- return internalClient.index(params);
+ return internalClient.index(
+ 'mark_configuration_applied_by_agent',
+ params
+ );
}
diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts
index 7454128a741d5..4e27953b3a315 100644
--- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts
+++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts
@@ -13,7 +13,6 @@ import {
import { Setup } from '../../helpers/setup_request';
import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types';
import { convertConfigSettingsToString } from './convert_settings_to_string';
-import { withApmSpan } from '../../../utils/with_apm_span';
export async function searchConfigurations({
service,
@@ -22,65 +21,64 @@ export async function searchConfigurations({
service: AgentConfiguration['service'];
setup: Setup;
}) {
- return withApmSpan('search_agent_configurations', async () => {
- const { internalClient, indices } = setup;
+ const { internalClient, indices } = setup;
- // In the following `constant_score` is being used to disable IDF calculation (where frequency of a term influences scoring).
- // Additionally a boost has been added to service.name to ensure it scores higher.
- // If there is tie between a config with a matching service.name and a config with a matching environment, the config that matches service.name wins
- const serviceNameFilter = service.name
- ? [
- {
- constant_score: {
- filter: { term: { [SERVICE_NAME]: service.name } },
- boost: 2,
- },
+ // In the following `constant_score` is being used to disable IDF calculation (where frequency of a term influences scoring).
+ // Additionally a boost has been added to service.name to ensure it scores higher.
+ // If there is tie between a config with a matching service.name and a config with a matching environment, the config that matches service.name wins
+ const serviceNameFilter = service.name
+ ? [
+ {
+ constant_score: {
+ filter: { term: { [SERVICE_NAME]: service.name } },
+ boost: 2,
},
- ]
- : [];
+ },
+ ]
+ : [];
- const environmentFilter = service.environment
- ? [
- {
- constant_score: {
- filter: { term: { [SERVICE_ENVIRONMENT]: service.environment } },
- boost: 1,
- },
+ const environmentFilter = service.environment
+ ? [
+ {
+ constant_score: {
+ filter: { term: { [SERVICE_ENVIRONMENT]: service.environment } },
+ boost: 1,
},
- ]
- : [];
+ },
+ ]
+ : [];
- const params = {
- index: indices.apmAgentConfigurationIndex,
- body: {
- query: {
- bool: {
- minimum_should_match: 2,
- should: [
- ...serviceNameFilter,
- ...environmentFilter,
- { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } },
- {
- bool: {
- must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }],
- },
+ const params = {
+ index: indices.apmAgentConfigurationIndex,
+ body: {
+ query: {
+ bool: {
+ minimum_should_match: 2,
+ should: [
+ ...serviceNameFilter,
+ ...environmentFilter,
+ { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } },
+ {
+ bool: {
+ must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }],
},
- ],
- },
+ },
+ ],
},
},
- };
+ },
+ };
- const resp = await internalClient.search(
- params
- );
+ const resp = await internalClient.search(
+ 'search_agent_configurations',
+ params
+ );
- const hit = resp.hits.hits[0] as SearchHit | undefined;
+ const hit = resp.hits.hits[0] as SearchHit | undefined;
- if (!hit) {
- return;
- }
+ if (!hit) {
+ return;
+ }
- return convertConfigSettingsToString(hit);
- });
+ return convertConfigSettingsToString(hit);
}
diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts
index 47ee91232ea48..051b9e2809e49 100644
--- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts
+++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts
@@ -39,17 +39,20 @@ describe('Create or Update Custom link', () => {
it('creates a new custom link', () => {
createOrUpdateCustomLink({ customLink, setup: mockedSetup });
- expect(internalClientIndexMock).toHaveBeenCalledWith({
- refresh: true,
- index: 'apmCustomLinkIndex',
- body: {
- '@timestamp': 1570737000000,
- label: 'foo',
- url: 'http://elastic.com/{{trace.id}}',
- 'service.name': ['opbeans-java'],
- 'transaction.type': ['Request'],
- },
- });
+ expect(internalClientIndexMock).toHaveBeenCalledWith(
+ 'create_or_update_custom_link',
+ {
+ refresh: true,
+ index: 'apmCustomLinkIndex',
+ body: {
+ '@timestamp': 1570737000000,
+ label: 'foo',
+ url: 'http://elastic.com/{{trace.id}}',
+ 'service.name': ['opbeans-java'],
+ 'transaction.type': ['Request'],
+ },
+ }
+ );
});
it('update a new custom link', () => {
createOrUpdateCustomLink({
@@ -57,17 +60,20 @@ describe('Create or Update Custom link', () => {
customLink,
setup: mockedSetup,
});
- expect(internalClientIndexMock).toHaveBeenCalledWith({
- refresh: true,
- index: 'apmCustomLinkIndex',
- id: 'bar',
- body: {
- '@timestamp': 1570737000000,
- label: 'foo',
- url: 'http://elastic.com/{{trace.id}}',
- 'service.name': ['opbeans-java'],
- 'transaction.type': ['Request'],
- },
- });
+ expect(internalClientIndexMock).toHaveBeenCalledWith(
+ 'create_or_update_custom_link',
+ {
+ refresh: true,
+ index: 'apmCustomLinkIndex',
+ id: 'bar',
+ body: {
+ '@timestamp': 1570737000000,
+ label: 'foo',
+ url: 'http://elastic.com/{{trace.id}}',
+ 'service.name': ['opbeans-java'],
+ 'transaction.type': ['Request'],
+ },
+ }
+ );
});
});
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 7e546fb555036..8f14e87fe183b 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
@@ -12,7 +12,6 @@ import {
import { Setup } from '../../helpers/setup_request';
import { toESFormat } from './helper';
import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client';
-import { withApmSpan } from '../../../utils/with_apm_span';
export function createOrUpdateCustomLink({
customLinkId,
@@ -23,23 +22,21 @@ export function createOrUpdateCustomLink({
customLink: Omit;
setup: Setup;
}) {
- return withApmSpan('create_or_update_custom_link', () => {
- const { internalClient, indices } = setup;
+ const { internalClient, indices } = setup;
- const params: APMIndexDocumentParams = {
- refresh: true,
- index: indices.apmCustomLinkIndex,
- body: {
- '@timestamp': Date.now(),
- ...toESFormat(customLink),
- },
- };
+ const params: APMIndexDocumentParams = {
+ refresh: true,
+ index: indices.apmCustomLinkIndex,
+ body: {
+ '@timestamp': Date.now(),
+ ...toESFormat(customLink),
+ },
+ };
- // by specifying an id elasticsearch will delete the previous doc and insert the updated doc
- if (customLinkId) {
- params.id = customLinkId;
- }
+ // by specifying an id elasticsearch will delete the previous doc and insert the updated doc
+ if (customLinkId) {
+ params.id = customLinkId;
+ }
- return internalClient.index(params);
- });
+ return internalClient.index('create_or_update_custom_link', params);
}
diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/delete_custom_link.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/delete_custom_link.ts
index 7c88bcc43cc7f..bf7cfb33d87ac 100644
--- a/x-pack/plugins/apm/server/lib/settings/custom_link/delete_custom_link.ts
+++ b/x-pack/plugins/apm/server/lib/settings/custom_link/delete_custom_link.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { withApmSpan } from '../../../utils/with_apm_span';
import { Setup } from '../../helpers/setup_request';
export function deleteCustomLink({
@@ -15,15 +14,13 @@ export function deleteCustomLink({
customLinkId: string;
setup: Setup;
}) {
- return withApmSpan('delete_custom_link', () => {
- const { internalClient, indices } = setup;
+ const { internalClient, indices } = setup;
- const params = {
- refresh: 'wait_for' as const,
- index: indices.apmCustomLinkIndex,
- id: customLinkId,
- };
+ const params = {
+ refresh: 'wait_for' as const,
+ index: indices.apmCustomLinkIndex,
+ id: customLinkId,
+ };
- return internalClient.delete(params);
- });
+ return internalClient.delete('delete_custom_link', params);
}
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 d3d9b45285354..91bc8c85bc014 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
@@ -11,43 +11,43 @@ import { Setup } from '../../helpers/setup_request';
import { ProcessorEvent } from '../../../../common/processor_event';
import { filterOptionsRt } from './custom_link_types';
import { splitFilterValueByComma } from './helper';
-import { withApmSpan } from '../../../utils/with_apm_span';
-export function getTransaction({
+export async function getTransaction({
setup,
filters = {},
}: {
setup: Setup;
filters?: t.TypeOf;
}) {
- return withApmSpan('get_transaction_for_custom_link', async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const esFilters = compact(
- Object.entries(filters)
- // loops through the filters splitting the value by comma and removing white spaces
- .map(([key, value]) => {
- if (value) {
- return { terms: { [key]: splitFilterValueByComma(value) } };
- }
- })
- );
+ const esFilters = compact(
+ Object.entries(filters)
+ // loops through the filters splitting the value by comma and removing white spaces
+ .map(([key, value]) => {
+ if (value) {
+ return { terms: { [key]: splitFilterValueByComma(value) } };
+ }
+ })
+ );
- const params = {
- terminateAfter: 1,
- apm: {
- events: [ProcessorEvent.transaction as const],
- },
- size: 1,
- body: {
- query: {
- bool: {
- filter: esFilters,
- },
+ const params = {
+ terminateAfter: 1,
+ apm: {
+ events: [ProcessorEvent.transaction as const],
+ },
+ size: 1,
+ body: {
+ query: {
+ bool: {
+ filter: esFilters,
},
},
- };
- const resp = await apmEventClient.search(params);
- return resp.hits.hits[0]?._source;
- });
+ },
+ };
+ const resp = await apmEventClient.search(
+ 'get_transaction_for_custom_link',
+ params
+ );
+ return resp.hits.hits[0]?._source;
}
diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts
index 0eac2e08d0901..d477da85e0d9b 100644
--- a/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts
+++ b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts
@@ -14,54 +14,54 @@ import {
import { Setup } from '../../helpers/setup_request';
import { fromESFormat } from './helper';
import { filterOptionsRt } from './custom_link_types';
-import { withApmSpan } from '../../../utils/with_apm_span';
-export function listCustomLinks({
+export async function listCustomLinks({
setup,
filters = {},
}: {
setup: Setup;
filters?: t.TypeOf;
}): Promise {
- return withApmSpan('list_custom_links', async () => {
- const { internalClient, indices } = setup;
- const esFilters = Object.entries(filters).map(([key, value]) => {
- return {
+ const { internalClient, indices } = setup;
+ const esFilters = Object.entries(filters).map(([key, value]) => {
+ return {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ { term: { [key]: value } },
+ { bool: { must_not: [{ exists: { field: key } }] } },
+ ] as QueryDslQueryContainer[],
+ },
+ };
+ });
+
+ const params = {
+ index: indices.apmCustomLinkIndex,
+ size: 500,
+ body: {
+ query: {
bool: {
- minimum_should_match: 1,
- should: [
- { term: { [key]: value } },
- { bool: { must_not: [{ exists: { field: key } }] } },
- ] as QueryDslQueryContainer[],
+ filter: esFilters,
},
- };
- });
-
- const params = {
- index: indices.apmCustomLinkIndex,
- size: 500,
- body: {
- query: {
- bool: {
- filter: esFilters,
+ },
+ sort: [
+ {
+ 'label.keyword': {
+ order: 'asc' as const,
},
},
- sort: [
- {
- 'label.keyword': {
- order: 'asc' as const,
- },
- },
- ],
- },
- };
- const resp = await internalClient.search(params);
- const customLinks = resp.hits.hits.map((item) =>
- fromESFormat({
- id: item._id,
- ...item._source,
- })
- );
- return customLinks;
- });
+ ],
+ },
+ };
+ const resp = await internalClient.search(
+ 'list_custom_links',
+ params
+ );
+ const customLinks = resp.hits.hits.map((item) =>
+ fromESFormat({
+ id: item._id,
+ ...item._source,
+ })
+ );
+ return customLinks;
}
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 157f09978eaec..68d316ef55df9 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
@@ -19,7 +19,6 @@ import { APMError } from '../../../typings/es_schemas/ui/apm_error';
import { rangeQuery } from '../../../server/utils/queries';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { PromiseValueType } from '../../../typings/common';
-import { withApmSpan } from '../../utils/with_apm_span';
export interface ErrorsPerTransaction {
[transactionId: string]: number;
@@ -29,103 +28,94 @@ export async function getTraceItems(
traceId: string,
setup: Setup & SetupTimeRange
) {
- return withApmSpan('get_trace_items', async () => {
- const { start, end, apmEventClient, config } = setup;
- const maxTraceItems = config['xpack.apm.ui.maxTraceItems'];
- const excludedLogLevels = ['debug', 'info', 'warning'];
+ const { start, end, apmEventClient, config } = setup;
+ const maxTraceItems = config['xpack.apm.ui.maxTraceItems'];
+ const excludedLogLevels = ['debug', 'info', 'warning'];
- const errorResponsePromise = withApmSpan('get_trace_error_items', () =>
- apmEventClient.search({
- apm: {
- events: [ProcessorEvent.error],
+ const errorResponsePromise = apmEventClient.search('get_trace_items', {
+ apm: {
+ events: [ProcessorEvent.error],
+ },
+ body: {
+ size: maxTraceItems,
+ query: {
+ bool: {
+ filter: [
+ { term: { [TRACE_ID]: traceId } },
+ ...rangeQuery(start, end),
+ ],
+ must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } },
},
- body: {
- size: maxTraceItems,
- query: {
- bool: {
- filter: [
- { term: { [TRACE_ID]: traceId } },
- ...rangeQuery(start, end),
- ],
- must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } },
- },
- },
- aggs: {
- by_transaction_id: {
- terms: {
- field: TRANSACTION_ID,
- size: maxTraceItems,
- // high cardinality
- execution_hint: 'map' as const,
- },
- },
+ },
+ aggs: {
+ by_transaction_id: {
+ terms: {
+ field: TRANSACTION_ID,
+ size: maxTraceItems,
+ // high cardinality
+ execution_hint: 'map' as const,
},
},
- })
- );
+ },
+ },
+ });
- const traceResponsePromise = withApmSpan('get_trace_span_items', () =>
- apmEventClient.search({
- apm: {
- events: [ProcessorEvent.span, ProcessorEvent.transaction],
- },
- body: {
- size: maxTraceItems,
- query: {
- bool: {
- filter: [
- { term: { [TRACE_ID]: traceId } },
- ...rangeQuery(start, end),
- ] as QueryDslQueryContainer[],
- should: {
- exists: { field: PARENT_ID },
- },
- },
+ const traceResponsePromise = apmEventClient.search('get_trace_span_items', {
+ apm: {
+ events: [ProcessorEvent.span, ProcessorEvent.transaction],
+ },
+ body: {
+ size: maxTraceItems,
+ query: {
+ bool: {
+ filter: [
+ { term: { [TRACE_ID]: traceId } },
+ ...rangeQuery(start, end),
+ ] as QueryDslQueryContainer[],
+ should: {
+ exists: { field: PARENT_ID },
},
- sort: [
- { _score: { order: 'asc' as const } },
- { [TRANSACTION_DURATION]: { order: 'desc' as const } },
- { [SPAN_DURATION]: { order: 'desc' as const } },
- ],
- track_total_hits: true,
},
- })
- );
+ },
+ sort: [
+ { _score: { order: 'asc' as const } },
+ { [TRANSACTION_DURATION]: { order: 'desc' as const } },
+ { [SPAN_DURATION]: { order: 'desc' as const } },
+ ],
+ track_total_hits: true,
+ },
+ });
- const [errorResponse, traceResponse]: [
- // explicit intermediary types to avoid TS "excessively deep" error
- PromiseValueType,
- PromiseValueType
- ] = (await Promise.all([
- errorResponsePromise,
- traceResponsePromise,
- ])) as any;
+ const [errorResponse, traceResponse]: [
+ // explicit intermediary types to avoid TS "excessively deep" error
+ PromiseValueType,
+ PromiseValueType
+ ] = (await Promise.all([errorResponsePromise, traceResponsePromise])) as any;
- const exceedsMax = traceResponse.hits.total.value > maxTraceItems;
+ const exceedsMax = traceResponse.hits.total.value > maxTraceItems;
- const items = traceResponse.hits.hits.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),
- errorsPerTransaction:
- errorResponse.aggregations?.by_transaction_id.buckets.reduce(
- (acc, current) => {
- return {
- ...acc,
- [current.key]: current.doc_count,
- };
- },
- {} as ErrorsPerTransaction
- ) ?? {},
- };
+ const errorFrequencies: {
+ errorsPerTransaction: ErrorsPerTransaction;
+ errorDocs: APMError[];
+ } = {
+ errorDocs: errorResponse.hits.hits.map(({ _source }) => _source),
+ errorsPerTransaction:
+ errorResponse.aggregations?.by_transaction_id.buckets.reduce(
+ (acc, current) => {
+ return {
+ ...acc,
+ [current.key]: current.doc_count,
+ };
+ },
+ {} as ErrorsPerTransaction
+ ) ?? {},
+ };
- return {
- items,
- exceedsMax,
- ...errorFrequencies,
- };
- });
+ return {
+ items,
+ exceedsMax,
+ ...errorFrequencies,
+ };
}
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 71f803a03bf85..6499e80be9302 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
@@ -31,7 +31,6 @@ import {
getOutcomeAggregation,
getTransactionErrorRateTimeSeries,
} from '../helpers/transaction_error_rate';
-import { withApmSpan } from '../../utils/with_apm_span';
export async function getErrorRate({
environment,
@@ -58,81 +57,82 @@ export async function getErrorRate({
transactionErrorRate: Coordinate[];
average: number | null;
}> {
- return withApmSpan('get_transaction_group_error_rate', async () => {
- const { apmEventClient } = setup;
-
- const transactionNamefilter = transactionName
- ? [{ term: { [TRANSACTION_NAME]: transactionName } }]
- : [];
- const transactionTypefilter = transactionType
- ? [{ term: { [TRANSACTION_TYPE]: transactionType } }]
- : [];
-
- const filter = [
- { term: { [SERVICE_NAME]: serviceName } },
- {
- terms: {
- [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success],
- },
- },
- ...transactionNamefilter,
- ...transactionTypefilter,
- ...getDocumentTypeFilterForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ...kqlQuery(kuery),
- ];
-
- const outcomes = getOutcomeAggregation();
-
- const params = {
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ],
+ const { apmEventClient } = setup;
+
+ const transactionNamefilter = transactionName
+ ? [{ term: { [TRANSACTION_NAME]: transactionName } }]
+ : [];
+ const transactionTypefilter = transactionType
+ ? [{ term: { [TRANSACTION_TYPE]: transactionType } }]
+ : [];
+
+ const filter = [
+ { term: { [SERVICE_NAME]: serviceName } },
+ {
+ terms: {
+ [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success],
},
- body: {
- size: 0,
- query: { bool: { filter } },
- aggs: {
- outcomes,
- timeseries: {
- date_histogram: {
- field: '@timestamp',
- fixed_interval: getBucketSize({ start, end }).intervalString,
- min_doc_count: 0,
- extended_bounds: { min: start, max: end },
- },
- aggs: {
- outcomes,
- },
+ },
+ ...transactionNamefilter,
+ ...transactionTypefilter,
+ ...getDocumentTypeFilterForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ ];
+
+ const outcomes = getOutcomeAggregation();
+
+ const params = {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ],
+ },
+ body: {
+ size: 0,
+ query: { bool: { filter } },
+ aggs: {
+ outcomes,
+ timeseries: {
+ date_histogram: {
+ field: '@timestamp',
+ fixed_interval: getBucketSize({ start, end }).intervalString,
+ min_doc_count: 0,
+ extended_bounds: { min: start, max: end },
+ },
+ aggs: {
+ outcomes,
},
},
},
- };
+ },
+ };
- const resp = await apmEventClient.search(params);
+ const resp = await apmEventClient.search(
+ 'get_transaction_group_error_rate',
+ params
+ );
- const noHits = resp.hits.total.value === 0;
+ const noHits = resp.hits.total.value === 0;
- if (!resp.aggregations) {
- return { noHits, transactionErrorRate: [], average: null };
- }
+ if (!resp.aggregations) {
+ return { noHits, transactionErrorRate: [], average: null };
+ }
- const transactionErrorRate = getTransactionErrorRateTimeSeries(
- resp.aggregations.timeseries.buckets
- );
+ const transactionErrorRate = getTransactionErrorRateTimeSeries(
+ resp.aggregations.timeseries.buckets
+ );
- const average = calculateTransactionErrorPercentage(
- resp.aggregations.outcomes
- );
+ const average = calculateTransactionErrorPercentage(
+ resp.aggregations.outcomes
+ );
- return { noHits, transactionErrorRate, average };
- });
+ return { noHits, transactionErrorRate, average };
}
export async function getErrorRatePeriods({
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 8156d52d984df..34fd86f2fc598 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
@@ -11,7 +11,6 @@ import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames';
import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable';
import { TransactionGroupRequestBase, TransactionGroupSetup } from './fetcher';
import { getTransactionDurationFieldForAggregatedTransactions } from '../helpers/aggregated_transactions';
-import { withApmSpan } from '../../utils/with_apm_span';
interface MetricParams {
request: TransactionGroupRequestBase;
@@ -39,124 +38,128 @@ function mergeRequestWithAggs<
});
}
-export function getAverages({
+export async function getAverages({
request,
setup,
searchAggregatedTransactions,
}: MetricParams) {
- return withApmSpan('get_avg_transaction_group_duration', async () => {
- const params = mergeRequestWithAggs(request, {
+ const params = mergeRequestWithAggs(request, {
+ avg: {
avg: {
- avg: {
- field: getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- },
+ field: getTransactionDurationFieldForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
},
- });
-
- const response = await setup.apmEventClient.search(params);
-
- return arrayUnionToCallable(
- response.aggregations?.transaction_groups.buckets ?? []
- ).map((bucket) => {
- return {
- key: bucket.key as BucketKey,
- avg: bucket.avg.value,
- };
- });
+ },
+ });
+
+ const response = await setup.apmEventClient.search(
+ 'get_avg_transaction_group_duration',
+ params
+ );
+
+ return arrayUnionToCallable(
+ response.aggregations?.transaction_groups.buckets ?? []
+ ).map((bucket) => {
+ return {
+ key: bucket.key as BucketKey,
+ avg: bucket.avg.value,
+ };
});
}
-export function getCounts({ request, setup }: MetricParams) {
- return withApmSpan('get_transaction_group_transaction_count', async () => {
- const params = mergeRequestWithAggs(request, {
- transaction_type: {
- top_metrics: {
- sort: {
- '@timestamp': 'desc' as const,
- },
- metrics: [
- {
- field: TRANSACTION_TYPE,
- } as const,
- ],
+export async function getCounts({ request, setup }: MetricParams) {
+ const params = mergeRequestWithAggs(request, {
+ transaction_type: {
+ top_metrics: {
+ sort: {
+ '@timestamp': 'desc' as const,
},
+ metrics: [
+ {
+ field: TRANSACTION_TYPE,
+ } as const,
+ ],
},
- });
-
- const response = await setup.apmEventClient.search(params);
-
- return arrayUnionToCallable(
- response.aggregations?.transaction_groups.buckets ?? []
- ).map((bucket) => {
- return {
- key: bucket.key as BucketKey,
- count: bucket.doc_count,
- transactionType: bucket.transaction_type.top[0].metrics[
- TRANSACTION_TYPE
- ] as string,
- };
- });
+ },
+ });
+
+ const response = await setup.apmEventClient.search(
+ 'get_transaction_group_transaction_count',
+ params
+ );
+
+ return arrayUnionToCallable(
+ response.aggregations?.transaction_groups.buckets ?? []
+ ).map((bucket) => {
+ return {
+ key: bucket.key as BucketKey,
+ count: bucket.doc_count,
+ transactionType: bucket.transaction_type.top[0].metrics[
+ TRANSACTION_TYPE
+ ] as string,
+ };
});
}
-export function getSums({
+export async function getSums({
request,
setup,
searchAggregatedTransactions,
}: MetricParams) {
- return withApmSpan('get_transaction_group_latency_sums', async () => {
- const params = mergeRequestWithAggs(request, {
+ const params = mergeRequestWithAggs(request, {
+ sum: {
sum: {
- sum: {
- field: getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- },
+ field: getTransactionDurationFieldForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
},
- });
-
- const response = await setup.apmEventClient.search(params);
-
- return arrayUnionToCallable(
- response.aggregations?.transaction_groups.buckets ?? []
- ).map((bucket) => {
- return {
- key: bucket.key as BucketKey,
- sum: bucket.sum.value,
- };
- });
+ },
+ });
+
+ const response = await setup.apmEventClient.search(
+ 'get_transaction_group_latency_sums',
+ params
+ );
+
+ return arrayUnionToCallable(
+ response.aggregations?.transaction_groups.buckets ?? []
+ ).map((bucket) => {
+ return {
+ key: bucket.key as BucketKey,
+ sum: bucket.sum.value,
+ };
});
}
-export function getPercentiles({
+export async function getPercentiles({
request,
setup,
searchAggregatedTransactions,
}: MetricParams) {
- return withApmSpan('get_transaction_group_latency_percentiles', async () => {
- const params = mergeRequestWithAggs(request, {
- p95: {
- percentiles: {
- field: getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- hdr: { number_of_significant_value_digits: 2 },
- percents: [95],
- },
+ const params = mergeRequestWithAggs(request, {
+ p95: {
+ percentiles: {
+ field: getTransactionDurationFieldForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ hdr: { number_of_significant_value_digits: 2 },
+ percents: [95],
},
- });
-
- const response = await setup.apmEventClient.search(params);
-
- return arrayUnionToCallable(
- response.aggregations?.transaction_groups.buckets ?? []
- ).map((bucket) => {
- return {
- key: bucket.key as BucketKey,
- p95: Object.values(bucket.p95.values)[0],
- };
- });
+ },
+ });
+
+ const response = await setup.apmEventClient.search(
+ 'get_transaction_group_latency_percentiles',
+ params
+ );
+
+ return arrayUnionToCallable(
+ response.aggregations?.transaction_groups.buckets ?? []
+ ).map((bucket) => {
+ return {
+ key: bucket.key as BucketKey,
+ p95: Object.values(bucket.p95.values)[0],
+ };
});
}
diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts
index 6cb85b62205b8..5c1754cd36ef4 100644
--- a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts
+++ b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts
@@ -33,7 +33,7 @@ describe('transaction group queries', () => {
)
);
- const allParams = mock.spy.mock.calls.map((call) => call[0]);
+ const allParams = mock.spy.mock.calls.map((call) => call[1]);
expect(allParams).toMatchSnapshot();
});
@@ -51,7 +51,7 @@ describe('transaction group queries', () => {
)
);
- const allParams = mock.spy.mock.calls.map((call) => call[0]);
+ const allParams = mock.spy.mock.calls.map((call) => call[1]);
expect(allParams).toMatchSnapshot();
});
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 568769b52e2b4..20534a5fa7cbf 100644
--- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts
@@ -26,9 +26,8 @@ import {
import { getMetricsDateHistogramParams } from '../../helpers/metrics';
import { MAX_KPIS } from './constants';
import { getVizColorForIndex } from '../../../../common/viz_colors';
-import { withApmSpan } from '../../../utils/with_apm_span';
-export function getTransactionBreakdown({
+export async function getTransactionBreakdown({
environment,
kuery,
setup,
@@ -43,205 +42,203 @@ export function getTransactionBreakdown({
transactionName?: string;
transactionType: string;
}) {
- return withApmSpan('get_transaction_breakdown', async () => {
- const { apmEventClient, start, end, config } = setup;
+ const { apmEventClient, start, end, config } = setup;
- const subAggs = {
- sum_all_self_times: {
- sum: {
- field: SPAN_SELF_TIME_SUM,
- },
+ const subAggs = {
+ sum_all_self_times: {
+ sum: {
+ field: SPAN_SELF_TIME_SUM,
},
- total_transaction_breakdown_count: {
- sum: {
- field: TRANSACTION_BREAKDOWN_COUNT,
- },
+ },
+ total_transaction_breakdown_count: {
+ sum: {
+ field: TRANSACTION_BREAKDOWN_COUNT,
},
- types: {
- terms: {
- field: SPAN_TYPE,
- size: 20,
- order: {
- _count: 'desc' as const,
- },
+ },
+ types: {
+ terms: {
+ field: SPAN_TYPE,
+ size: 20,
+ order: {
+ _count: 'desc' as const,
},
- aggs: {
- subtypes: {
- terms: {
- field: SPAN_SUBTYPE,
- missing: '',
- size: 20,
- order: {
- _count: 'desc' as const,
- },
+ },
+ aggs: {
+ subtypes: {
+ terms: {
+ field: SPAN_SUBTYPE,
+ missing: '',
+ size: 20,
+ order: {
+ _count: 'desc' as const,
},
- aggs: {
- total_self_time_per_subtype: {
- sum: {
- field: SPAN_SELF_TIME_SUM,
- },
+ },
+ aggs: {
+ total_self_time_per_subtype: {
+ sum: {
+ field: SPAN_SELF_TIME_SUM,
},
},
},
},
},
- };
-
- const filters = [
- { term: { [SERVICE_NAME]: serviceName } },
- { term: { [TRANSACTION_TYPE]: transactionType } },
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ...kqlQuery(kuery),
- {
+ },
+ };
+
+ const filters = [
+ { term: { [SERVICE_NAME]: serviceName } },
+ { term: { [TRANSACTION_TYPE]: transactionType } },
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ {
+ bool: {
+ should: [
+ { exists: { field: SPAN_SELF_TIME_SUM } },
+ { exists: { field: TRANSACTION_BREAKDOWN_COUNT } },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ ];
+
+ if (transactionName) {
+ filters.push({ term: { [TRANSACTION_NAME]: transactionName } });
+ }
+
+ const params = {
+ apm: {
+ events: [ProcessorEvent.metric],
+ },
+ body: {
+ size: 0,
+ query: {
bool: {
- should: [
- { exists: { field: SPAN_SELF_TIME_SUM } },
- { exists: { field: TRANSACTION_BREAKDOWN_COUNT } },
- ],
- minimum_should_match: 1,
+ filter: filters,
},
},
- ];
-
- if (transactionName) {
- filters.push({ term: { [TRANSACTION_NAME]: transactionName } });
- }
-
- const params = {
- apm: {
- events: [ProcessorEvent.metric],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter: filters,
- },
- },
- aggs: {
- ...subAggs,
- by_date: {
- date_histogram: getMetricsDateHistogramParams(
- start,
- end,
- config['xpack.apm.metricsInterval']
- ),
- aggs: subAggs,
- },
+ aggs: {
+ ...subAggs,
+ by_date: {
+ date_histogram: getMetricsDateHistogramParams(
+ start,
+ end,
+ config['xpack.apm.metricsInterval']
+ ),
+ aggs: subAggs,
},
},
+ },
+ };
+
+ const resp = await apmEventClient.search('get_transaction_breakdown', params);
+
+ const formatBucket = (
+ aggs:
+ | Required['aggregations']
+ | Required['aggregations']['by_date']['buckets'][0]
+ ) => {
+ const sumAllSelfTimes = aggs.sum_all_self_times.value || 0;
+
+ const breakdowns = flatten(
+ aggs.types.buckets.map((bucket) => {
+ const type = bucket.key as string;
+
+ return bucket.subtypes.buckets.map((subBucket) => {
+ return {
+ name: (subBucket.key as string) || type,
+ percentage:
+ (subBucket.total_self_time_per_subtype.value || 0) /
+ sumAllSelfTimes,
+ };
+ });
+ })
+ );
+
+ return breakdowns;
+ };
+
+ const visibleKpis = resp.aggregations
+ ? orderBy(formatBucket(resp.aggregations), 'percentage', 'desc').slice(
+ 0,
+ MAX_KPIS
+ )
+ : [];
+
+ const kpis = orderBy(
+ visibleKpis.map((kpi) => ({
+ ...kpi,
+ lowerCaseName: kpi.name.toLowerCase(),
+ })),
+ 'lowerCaseName'
+ ).map((kpi, index) => {
+ const { lowerCaseName, ...rest } = kpi;
+ return {
+ ...rest,
+ color: getVizColorForIndex(index),
};
+ });
- const resp = await apmEventClient.search(params);
-
- const formatBucket = (
- aggs:
- | Required['aggregations']
- | Required['aggregations']['by_date']['buckets'][0]
- ) => {
- const sumAllSelfTimes = aggs.sum_all_self_times.value || 0;
-
- const breakdowns = flatten(
- aggs.types.buckets.map((bucket) => {
- const type = bucket.key as string;
-
- return bucket.subtypes.buckets.map((subBucket) => {
- return {
- name: (subBucket.key as string) || type,
- percentage:
- (subBucket.total_self_time_per_subtype.value || 0) /
- sumAllSelfTimes,
- };
- });
- })
- );
-
- return breakdowns;
- };
-
- const visibleKpis = resp.aggregations
- ? orderBy(formatBucket(resp.aggregations), 'percentage', 'desc').slice(
- 0,
- MAX_KPIS
- )
- : [];
-
- const kpis = orderBy(
- visibleKpis.map((kpi) => ({
- ...kpi,
- lowerCaseName: kpi.name.toLowerCase(),
- })),
- 'lowerCaseName'
- ).map((kpi, index) => {
- const { lowerCaseName, ...rest } = kpi;
- return {
- ...rest,
- color: getVizColorForIndex(index),
- };
- });
-
- const kpiNames = kpis.map((kpi) => kpi.name);
+ const kpiNames = kpis.map((kpi) => kpi.name);
- const bucketsByDate = resp.aggregations?.by_date.buckets || [];
+ const bucketsByDate = resp.aggregations?.by_date.buckets || [];
- const timeseriesPerSubtype = bucketsByDate.reduce((prev, bucket) => {
- const formattedValues = formatBucket(bucket);
- const time = bucket.key;
+ const timeseriesPerSubtype = bucketsByDate.reduce((prev, bucket) => {
+ const formattedValues = formatBucket(bucket);
+ const time = bucket.key;
- const updatedSeries = kpiNames.reduce((p, kpiName) => {
- const { name, percentage } = formattedValues.find(
- (val) => val.name === kpiName
- ) || {
- name: kpiName,
- percentage: null,
- };
+ const updatedSeries = kpiNames.reduce((p, kpiName) => {
+ const { name, percentage } = formattedValues.find(
+ (val) => val.name === kpiName
+ ) || {
+ name: kpiName,
+ percentage: null,
+ };
- if (!p[name]) {
- p[name] = [];
- }
- return {
- ...p,
- [name]: p[name].concat({
- x: time,
- y: percentage,
- }),
- };
- }, prev);
-
- const lastValues = Object.values(updatedSeries).map(last);
-
- // If for a given timestamp, some series have data, but others do not,
- // we have to set any null values to 0 to make sure the stacked area chart
- // is drawn correctly.
- // If we set all values to 0, the chart always displays null values as 0,
- // and the chart looks weird.
- const hasAnyValues = lastValues.some((value) => value?.y !== null);
- const hasNullValues = lastValues.some((value) => value?.y === null);
-
- if (hasAnyValues && hasNullValues) {
- Object.values(updatedSeries).forEach((series) => {
- const value = series[series.length - 1];
- const isEmpty = value.y === null;
- if (isEmpty) {
- // local mutation to prevent complicated map/reduce calls
- value.y = 0;
- }
- });
+ if (!p[name]) {
+ p[name] = [];
}
+ return {
+ ...p,
+ [name]: p[name].concat({
+ x: time,
+ y: percentage,
+ }),
+ };
+ }, prev);
+
+ const lastValues = Object.values(updatedSeries).map(last);
+
+ // If for a given timestamp, some series have data, but others do not,
+ // we have to set any null values to 0 to make sure the stacked area chart
+ // is drawn correctly.
+ // If we set all values to 0, the chart always displays null values as 0,
+ // and the chart looks weird.
+ const hasAnyValues = lastValues.some((value) => value?.y !== null);
+ const hasNullValues = lastValues.some((value) => value?.y === null);
+
+ if (hasAnyValues && hasNullValues) {
+ Object.values(updatedSeries).forEach((series) => {
+ const value = series[series.length - 1];
+ const isEmpty = value.y === null;
+ if (isEmpty) {
+ // local mutation to prevent complicated map/reduce calls
+ value.y = 0;
+ }
+ });
+ }
- return updatedSeries;
- }, {} as Record>);
+ return updatedSeries;
+ }, {} as Record>);
- const timeseries = kpis.map((kpi) => ({
- title: kpi.name,
- color: kpi.color,
- type: 'areaStacked',
- data: timeseriesPerSubtype[kpi.name],
- hideLegend: false,
- legendValue: asPercent(kpi.percentage, 1),
- }));
+ const timeseries = kpis.map((kpi) => ({
+ title: kpi.name,
+ color: kpi.color,
+ type: 'areaStacked',
+ data: timeseriesPerSubtype[kpi.name],
+ hideLegend: false,
+ legendValue: asPercent(kpi.percentage, 1),
+ }));
- return { timeseries };
- });
+ return { timeseries };
}
diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts
index 3b4319c37996d..6259bb75386fb 100644
--- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts
@@ -89,48 +89,47 @@ export async function getBuckets({
] as QueryDslQueryContainer[];
async function getSamplesForDistributionBuckets() {
- const response = await withApmSpan(
+ const response = await apmEventClient.search(
'get_samples_for_latency_distribution_buckets',
- () =>
- apmEventClient.search({
- apm: {
- events: [ProcessorEvent.transaction],
- },
- body: {
- query: {
- bool: {
- filter: [
- ...commonFilters,
- { term: { [TRANSACTION_SAMPLED]: true } },
- ],
- should: [
- { term: { [TRACE_ID]: traceId } },
- { term: { [TRANSACTION_ID]: transactionId } },
- ] as QueryDslQueryContainer[],
- },
+ {
+ apm: {
+ events: [ProcessorEvent.transaction],
+ },
+ body: {
+ query: {
+ bool: {
+ filter: [
+ ...commonFilters,
+ { term: { [TRANSACTION_SAMPLED]: true } },
+ ],
+ should: [
+ { term: { [TRACE_ID]: traceId } },
+ { term: { [TRANSACTION_ID]: transactionId } },
+ ] as QueryDslQueryContainer[],
},
- aggs: {
- distribution: {
- histogram: getHistogramAggOptions({
- bucketSize,
- field: TRANSACTION_DURATION,
- distributionMax,
- }),
- aggs: {
- samples: {
- top_hits: {
- _source: [TRANSACTION_ID, TRACE_ID],
- size: 10,
- sort: {
- _score: 'desc' as const,
- },
+ },
+ aggs: {
+ distribution: {
+ histogram: getHistogramAggOptions({
+ bucketSize,
+ field: TRANSACTION_DURATION,
+ distributionMax,
+ }),
+ aggs: {
+ samples: {
+ top_hits: {
+ _source: [TRANSACTION_ID, TRACE_ID],
+ size: 10,
+ sort: {
+ _score: 'desc' as const,
},
},
},
},
},
},
- })
+ },
+ }
);
return (
@@ -148,41 +147,40 @@ export async function getBuckets({
}
async function getDistributionBuckets() {
- const response = await withApmSpan(
+ const response = await apmEventClient.search(
'get_latency_distribution_buckets',
- () =>
- apmEventClient.search({
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ],
- },
- body: {
- query: {
- bool: {
- filter: [
- ...commonFilters,
- ...getDocumentTypeFilterForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ],
- },
+ {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ],
+ },
+ body: {
+ query: {
+ bool: {
+ filter: [
+ ...commonFilters,
+ ...getDocumentTypeFilterForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ],
},
- aggs: {
- distribution: {
- histogram: getHistogramAggOptions({
- field: getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- bucketSize,
- distributionMax,
- }),
- },
+ },
+ aggs: {
+ distribution: {
+ histogram: getHistogramAggOptions({
+ field: getTransactionDurationFieldForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ bucketSize,
+ distributionMax,
+ }),
},
},
- })
+ },
+ }
);
return (
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 2e86f6bb84c81..f3d4e8f6dd92d 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
@@ -20,7 +20,6 @@ import {
rangeQuery,
kqlQuery,
} from '../../../../server/utils/queries';
-import { withApmSpan } from '../../../utils/with_apm_span';
export async function getDistributionMax({
environment,
@@ -39,44 +38,45 @@ export async function getDistributionMax({
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
}) {
- return withApmSpan('get_latency_distribution_max', async () => {
- const { start, end, apmEventClient } = setup;
+ const { start, end, apmEventClient } = setup;
- const params = {
- apm: {
- events: [
- getProcessorEventForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- ],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- { term: { [SERVICE_NAME]: serviceName } },
- { term: { [TRANSACTION_TYPE]: transactionType } },
- { term: { [TRANSACTION_NAME]: transactionName } },
- ...rangeQuery(start, end),
- ...environmentQuery(environment),
- ...kqlQuery(kuery),
- ],
- },
+ const params = {
+ apm: {
+ events: [
+ getProcessorEventForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
+ ],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { term: { [SERVICE_NAME]: serviceName } },
+ { term: { [TRANSACTION_TYPE]: transactionType } },
+ { term: { [TRANSACTION_NAME]: transactionName } },
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ ],
},
- aggs: {
- stats: {
- max: {
- field: getTransactionDurationFieldForAggregatedTransactions(
- searchAggregatedTransactions
- ),
- },
+ },
+ aggs: {
+ stats: {
+ max: {
+ field: getTransactionDurationFieldForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
},
},
},
- };
+ },
+ };
- const resp = await apmEventClient.search(params);
- return resp.aggregations?.stats.value ?? null;
- });
+ const resp = await apmEventClient.search(
+ 'get_latency_distribution_max',
+ params
+ );
+ return resp.aggregations?.stats.value ?? null;
}
diff --git a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts
index 5a58360597828..2d350090fa28b 100644
--- a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts
@@ -26,7 +26,6 @@ import {
} from '../../../lib/helpers/aggregated_transactions';
import { getBucketSize } from '../../../lib/helpers/get_bucket_size';
import { Setup, SetupTimeRange } from '../../../lib/helpers/setup_request';
-import { withApmSpan } from '../../../utils/with_apm_span';
import {
getLatencyAggregation,
getLatencyValue,
@@ -112,10 +111,10 @@ function searchLatency({
},
};
- return apmEventClient.search(params);
+ return apmEventClient.search('get_latency_charts', params);
}
-export function getLatencyTimeseries({
+export async function getLatencyTimeseries({
environment,
kuery,
serviceName,
@@ -138,40 +137,38 @@ export function getLatencyTimeseries({
start: number;
end: number;
}) {
- return withApmSpan('get_latency_charts', async () => {
- const response = await searchLatency({
- environment,
- kuery,
- serviceName,
- transactionType,
- transactionName,
- setup,
- searchAggregatedTransactions,
- latencyAggregationType,
- start,
- end,
- });
-
- if (!response.aggregations) {
- return { latencyTimeseries: [], overallAvgDuration: null };
- }
-
- return {
- overallAvgDuration:
- response.aggregations.overall_avg_duration.value || null,
- latencyTimeseries: response.aggregations.latencyTimeseries.buckets.map(
- (bucket) => {
- return {
- x: bucket.key,
- y: getLatencyValue({
- latencyAggregationType,
- aggregation: bucket.latency,
- }),
- };
- }
- ),
- };
+ const response = await searchLatency({
+ environment,
+ kuery,
+ serviceName,
+ transactionType,
+ transactionName,
+ setup,
+ searchAggregatedTransactions,
+ latencyAggregationType,
+ start,
+ end,
});
+
+ if (!response.aggregations) {
+ return { latencyTimeseries: [], overallAvgDuration: null };
+ }
+
+ return {
+ overallAvgDuration:
+ response.aggregations.overall_avg_duration.value || null,
+ latencyTimeseries: response.aggregations.latencyTimeseries.buckets.map(
+ (bucket) => {
+ return {
+ x: bucket.key,
+ y: getLatencyValue({
+ latencyAggregationType,
+ aggregation: bucket.latency,
+ }),
+ };
+ }
+ ),
+ };
}
export async function getLatencyPeriods({
diff --git a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts
index a0225eb47e584..f4d9236395252 100644
--- a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts
@@ -24,7 +24,6 @@ import {
} from '../../../lib/helpers/aggregated_transactions';
import { getBucketSize } from '../../../lib/helpers/get_bucket_size';
import { Setup, SetupTimeRange } from '../../../lib/helpers/setup_request';
-import { withApmSpan } from '../../../utils/with_apm_span';
import { getThroughputBuckets } from './transform';
export type ThroughputChartsResponse = PromiseReturnType<
@@ -96,7 +95,7 @@ function searchThroughput({
},
};
- return apmEventClient.search(params);
+ return apmEventClient.search('get_transaction_throughput_series', params);
}
export async function getThroughputCharts({
@@ -116,26 +115,24 @@ export async function getThroughputCharts({
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
}) {
- return withApmSpan('get_transaction_throughput_series', async () => {
- const { bucketSize, intervalString } = getBucketSize(setup);
+ const { bucketSize, intervalString } = getBucketSize(setup);
- const response = await searchThroughput({
- environment,
- kuery,
- serviceName,
- transactionType,
- transactionName,
- setup,
- searchAggregatedTransactions,
- intervalString,
- });
-
- return {
- throughputTimeseries: getThroughputBuckets({
- throughputResultBuckets: response.aggregations?.throughput.buckets,
- bucketSize,
- setupTimeRange: setup,
- }),
- };
+ const response = await searchThroughput({
+ environment,
+ kuery,
+ serviceName,
+ transactionType,
+ transactionName,
+ setup,
+ searchAggregatedTransactions,
+ intervalString,
});
+
+ return {
+ throughputTimeseries: getThroughputBuckets({
+ throughputResultBuckets: response.aggregations?.throughput.buckets,
+ bucketSize,
+ setupTimeRange: setup,
+ }),
+ };
}
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 6987ef0757734..c928b00cefb63 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
@@ -13,9 +13,8 @@ import { rangeQuery } from '../../../../server/utils/queries';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
import { ProcessorEvent } from '../../../../common/processor_event';
import { asMutableArray } from '../../../../common/utils/as_mutable_array';
-import { withApmSpan } from '../../../utils/with_apm_span';
-export function getTransaction({
+export async function getTransaction({
transactionId,
traceId,
setup,
@@ -24,27 +23,25 @@ export function getTransaction({
traceId?: string;
setup: Setup | (Setup & SetupTimeRange);
}) {
- return withApmSpan('get_transaction', async () => {
- const { apmEventClient } = setup;
+ const { apmEventClient } = setup;
- const resp = await apmEventClient.search({
- apm: {
- events: [ProcessorEvent.transaction],
- },
- body: {
- size: 1,
- query: {
- bool: {
- filter: asMutableArray([
- { term: { [TRANSACTION_ID]: transactionId } },
- ...(traceId ? [{ term: { [TRACE_ID]: traceId } }] : []),
- ...('start' in setup ? rangeQuery(setup.start, setup.end) : []),
- ]),
- },
+ const resp = await apmEventClient.search('get_transaction', {
+ apm: {
+ events: [ProcessorEvent.transaction],
+ },
+ body: {
+ size: 1,
+ query: {
+ bool: {
+ filter: asMutableArray([
+ { term: { [TRANSACTION_ID]: transactionId } },
+ ...(traceId ? [{ term: { [TRACE_ID]: traceId } }] : []),
+ ...('start' in setup ? rangeQuery(setup.start, setup.end) : []),
+ ]),
},
},
- });
-
- return resp.hits.hits[0]?._source;
+ },
});
+
+ 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 dfdad2f59a848..568ce16e7aedc 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
@@ -11,40 +11,43 @@ import {
} from '../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../helpers/setup_request';
import { ProcessorEvent } from '../../../../common/processor_event';
-import { withApmSpan } from '../../../utils/with_apm_span';
-export function getRootTransactionByTraceId(traceId: string, setup: Setup) {
- return withApmSpan('get_root_transaction_by_trace_id', async () => {
- const { apmEventClient } = setup;
+export async function getRootTransactionByTraceId(
+ traceId: string,
+ setup: Setup
+) {
+ const { apmEventClient } = setup;
- const params = {
- apm: {
- events: [ProcessorEvent.transaction as const],
- },
- body: {
- size: 1,
- query: {
- bool: {
- should: [
- {
- constant_score: {
- filter: {
- bool: {
- must_not: { exists: { field: PARENT_ID } },
- },
+ const params = {
+ apm: {
+ events: [ProcessorEvent.transaction as const],
+ },
+ body: {
+ size: 1,
+ query: {
+ bool: {
+ should: [
+ {
+ constant_score: {
+ filter: {
+ bool: {
+ must_not: { exists: { field: PARENT_ID } },
},
},
},
- ],
- filter: [{ term: { [TRACE_ID]: traceId } }],
- },
+ },
+ ],
+ filter: [{ term: { [TRACE_ID]: traceId } }],
},
},
- };
+ },
+ };
- const resp = await apmEventClient.search(params);
- return {
- transaction: resp.hits.hits[0]?._source,
- };
- });
+ const resp = await apmEventClient.search(
+ 'get_root_transaction_by_trace_id',
+ params
+ );
+ return {
+ transaction: resp.hits.hits[0]?._source,
+ };
}
diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx
index 6252c33c5994d..8e1d971c9cd2e 100644
--- a/x-pack/plugins/apm/server/utils/test_helpers.tsx
+++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx
@@ -17,6 +17,7 @@ interface Options {
mockResponse?: (
request: ESSearchRequest
) => ESSearchResponse;
+ uiFilters?: Record;
}
interface MockSetup {
@@ -86,7 +87,7 @@ export async function inspectSearchParams(
},
}
) as APMConfig,
- uiFilters: {},
+ uiFilters: options?.uiFilters ?? {},
indices: {
/* eslint-disable @typescript-eslint/naming-convention */
'apm_oss.sourcemapIndices': 'myIndex',
@@ -109,7 +110,7 @@ export async function inspectSearchParams(
}
return {
- params: spy.mock.calls[0][0],
+ params: spy.mock.calls[0][1],
response,
error,
spy,
diff --git a/x-pack/plugins/apm/server/utils/with_apm_span.ts b/x-pack/plugins/apm/server/utils/with_apm_span.ts
index 9762a7213d0a2..1343970f04a3f 100644
--- a/x-pack/plugins/apm/server/utils/with_apm_span.ts
+++ b/x-pack/plugins/apm/server/utils/with_apm_span.ts
@@ -13,7 +13,7 @@ export function withApmSpan(
const options = parseSpanOptions(optionsOrName);
const optionsWithDefaults = {
- type: 'plugin:apm',
+ ...(options.intercept ? {} : { type: 'plugin:apm' }),
...options,
labels: {
plugin: 'apm',
diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts
index 284f5e706292c..1dbb633e32adf 100644
--- a/x-pack/plugins/cases/common/ui/types.ts
+++ b/x-pack/plugins/cases/common/ui/types.ts
@@ -153,7 +153,7 @@ export interface ActionLicense {
export interface DeleteCase {
id: string;
type: CaseType | null;
- title?: string;
+ title: string;
}
export interface FieldMappings {
diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts
index 85cfb60b1d6b8..f1bfde4cc4485 100644
--- a/x-pack/plugins/cases/public/common/translations.ts
+++ b/x-pack/plugins/cases/public/common/translations.ts
@@ -30,13 +30,11 @@ export const CANCEL = i18n.translate('xpack.cases.caseView.cancel', {
defaultMessage: 'Cancel',
});
-export const DELETE_CASE = i18n.translate('xpack.cases.confirmDeleteCase.deleteCase', {
- defaultMessage: 'Delete case',
-});
-
-export const DELETE_CASES = i18n.translate('xpack.cases.confirmDeleteCase.deleteCases', {
- defaultMessage: 'Delete cases',
-});
+export const DELETE_CASE = (quantity: number = 1) =>
+ i18n.translate('xpack.cases.confirmDeleteCase.deleteCase', {
+ values: { quantity },
+ defaultMessage: `Delete {quantity, plural, =1 {case} other {cases}}`,
+ });
export const NAME = i18n.translate('xpack.cases.caseView.name', {
defaultMessage: 'Name',
diff --git a/x-pack/plugins/cases/public/components/all_cases/actions.tsx b/x-pack/plugins/cases/public/components/all_cases/actions.tsx
index 8742b8fea23a4..4820b10308934 100644
--- a/x-pack/plugins/cases/public/components/all_cases/actions.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/actions.tsx
@@ -80,9 +80,9 @@ export const getActions = ({
makeInProgressAction,
closeCaseAction,
{
- description: i18n.DELETE_CASE,
+ description: i18n.DELETE_CASE(),
icon: 'trash',
- name: i18n.DELETE_CASE,
+ name: i18n.DELETE_CASE(),
onClick: deleteCaseOnClick,
type: 'icon',
'data-test-subj': 'action-delete',
diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx
index 947d405d188cf..a5a299851d975 100644
--- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx
@@ -306,7 +306,6 @@ export const useCasesColumns = ({
diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx
index d0981c38385e9..a2b4c14c0278a 100644
--- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx
@@ -41,12 +41,8 @@ export const CasesTableUtilityBar: FunctionComponent = ({
refreshCases,
selectedCases,
}) => {
- const [deleteBulk, setDeleteBulk] = useState([]);
- const [deleteThisCase, setDeleteThisCase] = useState({
- title: '',
- id: '',
- type: null,
- });
+ const [deleteCases, setDeleteCases] = useState([]);
+
// Delete case
const {
dispatchResetIsDeleted,
@@ -86,24 +82,15 @@ export const CasesTableUtilityBar: FunctionComponent = ({
const toggleBulkDeleteModal = useCallback(
(cases: Case[]) => {
handleToggleModal();
- if (cases.length === 1) {
- const singleCase = cases[0];
- if (singleCase) {
- return setDeleteThisCase({
- id: singleCase.id,
- title: singleCase.title,
- type: singleCase.type,
- });
- }
- }
+
const convertToDeleteCases: DeleteCase[] = cases.map(({ id, title, type }) => ({
id,
title,
type,
}));
- setDeleteBulk(convertToDeleteCases);
+ setDeleteCases(convertToDeleteCases);
},
- [setDeleteBulk, handleToggleModal]
+ [setDeleteCases, handleToggleModal]
);
const handleUpdateCaseStatus = useCallback(
@@ -128,6 +115,7 @@ export const CasesTableUtilityBar: FunctionComponent = ({
),
[selectedCases, filterOptions.status, toggleBulkDeleteModal, handleUpdateCaseStatus]
);
+
return (
@@ -159,14 +147,11 @@ export const CasesTableUtilityBar: FunctionComponent = ({
0}
+ caseQuantity={deleteCases.length}
onCancel={handleToggleModal}
- onConfirm={handleOnDeleteConfirm.bind(
- null,
- deleteBulk.length > 0 ? deleteBulk : [deleteThisCase]
- )}
+ onConfirm={handleOnDeleteConfirm.bind(null, deleteCases)}
/>
);
diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx
index 922ffd09aaac9..c2578dc3debdb 100644
--- a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx
+++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx
@@ -41,7 +41,7 @@ const ActionsComponent: React.FC = ({
{
disabled,
iconType: 'trash',
- label: i18n.DELETE_CASE,
+ label: i18n.DELETE_CASE(),
onClick: handleToggleModal,
},
...(currentExternalIncident != null && !isEmpty(currentExternalIncident?.externalUrl)
@@ -67,7 +67,6 @@ const ActionsComponent: React.FC = ({
void;
onConfirm: () => void;
}
@@ -20,7 +20,7 @@ interface ConfirmDeleteCaseModalProps {
const ConfirmDeleteCaseModalComp: React.FC = ({
caseTitle,
isModalVisible,
- isPlural,
+ caseQuantity = 1,
onCancel,
onConfirm,
}) => {
@@ -31,20 +31,14 @@ const ConfirmDeleteCaseModalComp: React.FC = ({
- {isPlural ? i18n.CONFIRM_QUESTION_PLURAL : i18n.CONFIRM_QUESTION}
+ {i18n.CONFIRM_QUESTION(caseQuantity)}
);
};
diff --git a/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts b/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts
index 0400c4c7fef41..f8e4ab2a83a73 100644
--- a/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts
+++ b/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts
@@ -14,23 +14,15 @@ export const DELETE_TITLE = (caseTitle: string) =>
defaultMessage: 'Delete "{caseTitle}"',
});
-export const DELETE_THIS_CASE = (caseTitle: string) =>
- i18n.translate('xpack.cases.confirmDeleteCase.deleteThisCase', {
- defaultMessage: 'Delete this case',
+export const DELETE_SELECTED_CASES = (quantity: number, title: string) =>
+ i18n.translate('xpack.cases.confirmDeleteCase.selectedCases', {
+ values: { quantity, title },
+ defaultMessage: 'Delete "{quantity, plural, =1 {{title}} other {Selected {quantity} cases}}"',
});
-export const CONFIRM_QUESTION = i18n.translate('xpack.cases.confirmDeleteCase.confirmQuestion', {
- defaultMessage:
- 'By deleting this case, all related case data will be permanently removed and you will no longer be able to push data to an external incident management system. Are you sure you wish to proceed?',
-});
-export const DELETE_SELECTED_CASES = i18n.translate('xpack.cases.confirmDeleteCase.selectedCases', {
- defaultMessage: 'Delete selected cases',
-});
-
-export const CONFIRM_QUESTION_PLURAL = i18n.translate(
- 'xpack.cases.confirmDeleteCase.confirmQuestionPlural',
- {
+export const CONFIRM_QUESTION = (quantity: number) =>
+ i18n.translate('xpack.cases.confirmDeleteCase.confirmQuestion', {
+ values: { quantity },
defaultMessage:
- 'By deleting these cases, all related case data will be permanently removed and you will no longer be able to push data to an external incident management system. Are you sure you wish to proceed?',
- }
-);
+ 'By deleting {quantity, plural, =1 {this case} other {these cases}}, all related case data will be permanently removed and you will no longer be able to push data to an external incident management system. Are you sure you wish to proceed?',
+ });
diff --git a/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx
index e86ed0c036974..691af580b333a 100644
--- a/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx
+++ b/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx
@@ -17,9 +17,9 @@ jest.mock('../common/lib/kibana');
describe('useDeleteCases', () => {
const abortCtrl = new AbortController();
const deleteObj = [
- { id: '1', type: CaseType.individual },
- { id: '2', type: CaseType.individual },
- { id: '3', type: CaseType.individual },
+ { id: '1', type: CaseType.individual, title: 'case 1' },
+ { id: '2', type: CaseType.individual, title: 'case 2' },
+ { id: '3', type: CaseType.individual, title: 'case 3' },
];
const deleteArr = ['1', '2', '3'];
it('init', async () => {
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx
index 0e5be4b5a51e9..c398c92abf3a9 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx
@@ -5,25 +5,15 @@
* 2.0.
*/
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiHorizontalRule,
- EuiPageBody,
- EuiPageContent,
- EuiSpacer,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiButtonEmpty, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import type { CoreStart, HttpStart } from 'kibana/public';
import React from 'react';
import type { SessionsConfigSchema } from '../';
+import { IManagementSectionsPluginsSetup } from '../';
import type { SearchSessionsMgmtAPI } from '../lib/api';
import type { AsyncSearchIntroDocumentation } from '../lib/documentation';
-import { TableText } from './';
import { SearchSessionsMgmtTable } from './table';
-import { IManagementSectionsPluginsSetup } from '../';
interface Props {
documentation: AsyncSearchIntroDocumentation;
@@ -37,46 +27,37 @@ interface Props {
export function SearchSessionsMgmtMain({ documentation, ...tableProps }: Props) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+ }
+ description={
+
+ }
+ bottomBorder
+ rightSideItems={[
+
-
-
-
-
+ ,
+ ]}
+ />
-
-
-
+
+
+ >
);
}
diff --git a/x-pack/plugins/encrypted_saved_objects/kibana.json b/x-pack/plugins/encrypted_saved_objects/kibana.json
index 74f797ba36a11..4812afe8c2072 100644
--- a/x-pack/plugins/encrypted_saved_objects/kibana.json
+++ b/x-pack/plugins/encrypted_saved_objects/kibana.json
@@ -1,5 +1,10 @@
{
"id": "encryptedSavedObjects",
+ "owner": {
+ "name": "Platform Security",
+ "githubTeam": "kibana-security"
+ },
+ "description": "This plugin provides encryption and decryption utilities for saved objects containing sensitive information.",
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "encryptedSavedObjects"],
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.test.tsx
new file mode 100644
index 0000000000000..8ad7b10685488
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.test.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { FormattedRelative } from '@kbn/i18n/react';
+
+import { FormattedDateTime } from '../../../utils/formatted_date_time';
+
+import { CustomFormattedTimestamp } from './custom_formatted_timestamp';
+
+describe('CustomFormattedTimestamp', () => {
+ const mockToday = jest
+ .spyOn(global.Date, 'now')
+ .mockImplementation(() => new Date('1970-01-02').valueOf());
+
+ afterAll(() => mockToday.mockRestore());
+
+ it('uses a relative time format (x minutes ago) if the timestamp is from today', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(FormattedRelative).prop('value')).toEqual(new Date('1970-01-02'));
+ });
+
+ it('uses a date if the timestamp is before today', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(FormattedDateTime).prop('date')).toEqual(new Date('1970-01-01'));
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.tsx
new file mode 100644
index 0000000000000..da6d1ac9f2cb1
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { FormattedRelative } from '@kbn/i18n/react';
+
+import { FormattedDateTime } from '../../../utils/formatted_date_time';
+
+interface CustomFormattedTimestampProps {
+ timestamp: string;
+}
+
+export const CustomFormattedTimestamp: React.FC = ({
+ timestamp,
+}) => {
+ const date = new Date(timestamp);
+ const isDateToday = date >= new Date(new Date(Date.now()).toDateString());
+ return isDateToday ? (
+
+ ) : (
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx
new file mode 100644
index 0000000000000..aab2909d630ed
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx
@@ -0,0 +1,154 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { mockKibanaValues, setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
+import '../../../__mocks__/engine_logic.mock';
+
+import React from 'react';
+
+import { shallow, ShallowWrapper } from 'enzyme';
+
+import { EuiBasicTable, EuiButtonIcon, EuiInMemoryTable } from '@elastic/eui';
+
+import { mountWithIntl } from '../../../../test_helpers';
+
+import { CrawlerDomain } from '../types';
+
+import { DomainsTable } from './domains_table';
+
+const domains: CrawlerDomain[] = [
+ {
+ id: '1234',
+ documentCount: 9999,
+ url: 'elastic.co',
+ crawlRules: [],
+ entryPoints: [],
+ sitemaps: [],
+ lastCrawl: '2020-01-01T00:00:00-05:00',
+ createdOn: '2020-01-01T00:00:00-05:00',
+ },
+];
+
+const values = {
+ // EngineLogic
+ engineName: 'some-engine',
+ // CrawlerOverviewLogic
+ domains,
+ // AppLogic
+ myRole: { canManageEngineCrawler: false },
+};
+
+const actions = {
+ // CrawlerOverviewLogic
+ deleteDomain: jest.fn(),
+};
+
+describe('DomainsTable', () => {
+ let wrapper: ShallowWrapper;
+ let tableContent: string;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ beforeAll(() => {
+ setMockValues(values);
+ setMockActions(actions);
+ wrapper = shallow( );
+ tableContent = mountWithIntl( )
+ .find(EuiInMemoryTable)
+ .text();
+ });
+
+ it('renders', () => {
+ expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1);
+ });
+
+ describe('columns', () => {
+ const getTable = () => wrapper.find(EuiInMemoryTable).dive().find(EuiBasicTable).dive();
+
+ beforeEach(() => {
+ wrapper = shallow( );
+ tableContent = mountWithIntl( )
+ .find(EuiInMemoryTable)
+ .text();
+ });
+
+ it('renders a url column', () => {
+ expect(tableContent).toContain('elastic.co');
+ });
+
+ it('renders a last crawled column', () => {
+ expect(tableContent).toContain('Last activity');
+ expect(tableContent).toContain('Jan 1, 2020');
+ });
+
+ it('renders a document count column', () => {
+ expect(tableContent).toContain('Documents');
+ expect(tableContent).toContain('9,999');
+ });
+
+ describe('actions column', () => {
+ const getActions = () => getTable().find('ExpandedItemActions');
+ const getActionItems = () => getActions().dive().find('DefaultItemAction');
+
+ it('will hide the action buttons if the user cannot manage/delete engines', () => {
+ setMockValues({
+ ...values,
+ // AppLogic
+ myRole: { canManageEngineCrawler: false },
+ });
+ wrapper = shallow( );
+
+ expect(getActions()).toHaveLength(0);
+ });
+
+ describe('when the user can manage/delete engines', () => {
+ const getManageAction = () => getActionItems().at(0).dive().find(EuiButtonIcon);
+ const getDeleteAction = () => getActionItems().at(1).dive().find(EuiButtonIcon);
+
+ beforeEach(() => {
+ setMockValues({
+ ...values,
+ // AppLogic
+ myRole: { canManageEngineCrawler: true },
+ });
+ wrapper = shallow( );
+ });
+
+ describe('manage action', () => {
+ it('sends the user to the engine overview on click', () => {
+ const { navigateToUrl } = mockKibanaValues;
+
+ getManageAction().simulate('click');
+
+ expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/crawler/domains/1234');
+ });
+ });
+
+ describe('delete action', () => {
+ it('clicking the action and confirming deletes the domain', () => {
+ jest.spyOn(global, 'confirm').mockReturnValueOnce(true);
+
+ getDeleteAction().simulate('click');
+
+ expect(actions.deleteDomain).toHaveBeenCalledWith(
+ expect.objectContaining({ id: '1234' })
+ );
+ });
+
+ it('clicking the action and not confirming does not delete the engine', () => {
+ jest.spyOn(global, 'confirm').mockReturnValueOnce(false);
+
+ getDeleteAction().simulate('click');
+
+ expect(actions.deleteDomain).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx
new file mode 100644
index 0000000000000..0da3b8462cfcf
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { useActions, useValues } from 'kea';
+
+import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
+import { FormattedNumber } from '@kbn/i18n/react';
+
+import { DELETE_BUTTON_LABEL, MANAGE_BUTTON_LABEL } from '../../../../shared/constants';
+import { KibanaLogic } from '../../../../shared/kibana';
+import { AppLogic } from '../../../app_logic';
+import { ENGINE_CRAWLER_DOMAIN_PATH } from '../../../routes';
+import { generateEnginePath } from '../../engine';
+import { CrawlerOverviewLogic } from '../crawler_overview_logic';
+import { CrawlerDomain } from '../types';
+
+import { CustomFormattedTimestamp } from './custom_formatted_timestamp';
+
+export const DomainsTable: React.FC = () => {
+ const { domains } = useValues(CrawlerOverviewLogic);
+
+ const { deleteDomain } = useActions(CrawlerOverviewLogic);
+
+ const {
+ myRole: { canManageEngineCrawler },
+ } = useValues(AppLogic);
+
+ const columns: Array> = [
+ {
+ field: 'url',
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.column.domainURL',
+ {
+ defaultMessage: 'Domain URL',
+ }
+ ),
+ },
+ {
+ field: 'lastCrawl',
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.column.lastActivity',
+ {
+ defaultMessage: 'Last activity',
+ }
+ ),
+ render: (lastCrawl: CrawlerDomain['lastCrawl']) =>
+ lastCrawl ? : '',
+ },
+ {
+ field: 'documentCount',
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.column.documents',
+ {
+ defaultMessage: 'Documents',
+ }
+ ),
+ render: (documentCount: CrawlerDomain['documentCount']) => (
+
+ ),
+ },
+ ];
+
+ const actionsColumn: EuiBasicTableColumn = {
+ name: i18n.translate('xpack.enterpriseSearch.appSearch.crawler.domainsTable.column.actions', {
+ defaultMessage: 'Actions',
+ }),
+ actions: [
+ {
+ name: MANAGE_BUTTON_LABEL,
+ description: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.manage.buttonLabel',
+ {
+ defaultMessage: 'Manage this domain',
+ }
+ ),
+ type: 'icon',
+ icon: 'eye',
+ onClick: (domain) =>
+ KibanaLogic.values.navigateToUrl(
+ generateEnginePath(ENGINE_CRAWLER_DOMAIN_PATH, { domainId: domain.id })
+ ),
+ },
+ {
+ name: DELETE_BUTTON_LABEL,
+ description: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.delete.buttonLabel',
+ {
+ defaultMessage: 'Delete this domain',
+ }
+ ),
+ type: 'icon',
+ icon: 'trash',
+ color: 'danger',
+ onClick: (domain) => {
+ if (
+ window.confirm(
+ i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.delete.confirmationPopupMessage',
+ {
+ defaultMessage:
+ 'Are you sure you want to remove the domain "{domainUrl}" and all of its settings?',
+ values: {
+ domainUrl: domain.url,
+ },
+ }
+ )
+ )
+ ) {
+ deleteDomain(domain);
+ }
+ },
+ },
+ ],
+ };
+
+ if (canManageEngineCrawler) {
+ columns.push(actionsColumn);
+ }
+
+ return ;
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
index cc3fa21f73309..affc2fd08e34c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
@@ -12,53 +12,46 @@ import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
-import { EuiCode } from '@elastic/eui';
-
import { Loading } from '../../../shared/loading';
import { rerender } from '../../../test_helpers';
+import { DomainsTable } from './components/domains_table';
import { CrawlerOverview } from './crawler_overview';
-const actions = {
- fetchCrawlerData: jest.fn(),
-};
+describe('CrawlerOverview', () => {
+ const mockActions = {
+ fetchCrawlerData: jest.fn(),
+ };
-const values = {
- dataLoading: false,
- domains: [],
-};
+ const mockValues = {
+ dataLoading: false,
+ domains: [],
+ };
-describe('CrawlerOverview', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
jest.clearAllMocks();
- setMockValues(values);
- setMockActions(actions);
+ setMockValues(mockValues);
+ setMockActions(mockActions);
wrapper = shallow( );
});
- it('renders', () => {
- expect(wrapper.find(EuiCode)).toHaveLength(1);
- });
-
it('calls fetchCrawlerData on page load', () => {
- expect(actions.fetchCrawlerData).toHaveBeenCalledTimes(1);
+ expect(mockActions.fetchCrawlerData).toHaveBeenCalledTimes(1);
});
- // TODO after DomainsTable is built in a future PR
- // it('contains a DomainsTable', () => {})
+ it('renders', () => {
+ expect(wrapper.find(DomainsTable)).toHaveLength(1);
- // TODO after CrawlRequestsTable is built in a future PR
- // it('containss a CrawlRequestsTable,() => {})
+ // TODO test for CrawlRequestsTable after it is built in a future PR
- // TODO after AddDomainForm is built in a future PR
- // it('contains an AddDomainForm' () => {})
+ // TODO test for AddDomainForm after it is built in a future PR
- // TODO after empty state is added in a future PR
- // it('has an empty state', () => {} )
+ // TODO test for empty state after it is built in a future PR
+ });
- it('shows an empty state when data is loading', () => {
+ it('shows a loading state when data is loading', () => {
setMockValues({ dataLoading: true });
rerender(wrapper);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
index 5eeaaaef69605..14906378692ed 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
@@ -9,17 +9,17 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
-import { EuiCode, EuiPageHeader } from '@elastic/eui';
+import { EuiPageHeader } from '@elastic/eui';
import { FlashMessages } from '../../../shared/flash_messages';
-
import { Loading } from '../../../shared/loading';
+import { DomainsTable } from './components/domains_table';
import { CRAWLER_TITLE } from './constants';
import { CrawlerOverviewLogic } from './crawler_overview_logic';
export const CrawlerOverview: React.FC = () => {
- const { dataLoading, domains } = useValues(CrawlerOverviewLogic);
+ const { dataLoading } = useValues(CrawlerOverviewLogic);
const { fetchCrawlerData } = useActions(CrawlerOverviewLogic);
@@ -35,7 +35,7 @@ export const CrawlerOverview: React.FC = () => {
<>
- {JSON.stringify(domains, null, 2)}
+
>
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts
index f23322337766a..7ef5984960e26 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts
@@ -15,7 +15,14 @@ import '../../__mocks__/engine_logic.mock';
import { nextTick } from '@kbn/test/jest';
import { CrawlerOverviewLogic } from './crawler_overview_logic';
-import { CrawlerPolicies, CrawlerRules, CrawlRule } from './types';
+import {
+ CrawlerDataFromServer,
+ CrawlerDomain,
+ CrawlerPolicies,
+ CrawlerRules,
+ CrawlRule,
+} from './types';
+import { crawlerDataServerToClient } from './utils';
const DEFAULT_VALUES = {
dataLoading: true,
@@ -29,10 +36,26 @@ const DEFAULT_CRAWL_RULE: CrawlRule = {
pattern: '.*',
};
+const MOCK_SERVER_DATA: CrawlerDataFromServer = {
+ domains: [
+ {
+ id: '507f1f77bcf86cd799439011',
+ name: 'elastic.co',
+ created_on: 'Mon, 31 Aug 2020 17:00:00 +0000',
+ document_count: 13,
+ sitemaps: [],
+ entry_points: [],
+ crawl_rules: [],
+ },
+ ],
+};
+
+const MOCK_CLIENT_DATA = crawlerDataServerToClient(MOCK_SERVER_DATA);
+
describe('CrawlerOverviewLogic', () => {
const { mount } = new LogicMounter(CrawlerOverviewLogic);
const { http } = mockHttpValues;
- const { flashAPIErrors } = mockFlashMessageHelpers;
+ const { flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers;
beforeEach(() => {
jest.clearAllMocks();
@@ -44,13 +67,13 @@ describe('CrawlerOverviewLogic', () => {
});
describe('actions', () => {
- describe('onFetchCrawlerData', () => {
+ describe('onReceiveCrawlerData', () => {
const crawlerData = {
domains: [
{
id: '507f1f77bcf86cd799439011',
createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000',
- url: 'moviedatabase.com',
+ url: 'elastic.co',
documentCount: 13,
sitemaps: [],
entryPoints: [],
@@ -61,7 +84,7 @@ describe('CrawlerOverviewLogic', () => {
};
beforeEach(() => {
- CrawlerOverviewLogic.actions.onFetchCrawlerData(crawlerData);
+ CrawlerOverviewLogic.actions.onReceiveCrawlerData(crawlerData);
});
it('should set all received data as top-level values', () => {
@@ -76,41 +99,17 @@ describe('CrawlerOverviewLogic', () => {
describe('listeners', () => {
describe('fetchCrawlerData', () => {
- it('calls onFetchCrawlerData with retrieved data that has been converted from server to client', async () => {
- jest.spyOn(CrawlerOverviewLogic.actions, 'onFetchCrawlerData');
-
- http.get.mockReturnValue(
- Promise.resolve({
- domains: [
- {
- id: '507f1f77bcf86cd799439011',
- name: 'moviedatabase.com',
- created_on: 'Mon, 31 Aug 2020 17:00:00 +0000',
- document_count: 13,
- sitemaps: [],
- entry_points: [],
- crawl_rules: [],
- },
- ],
- })
- );
+ it('calls onReceiveCrawlerData with retrieved data that has been converted from server to client', async () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'onReceiveCrawlerData');
+
+ http.get.mockReturnValue(Promise.resolve(MOCK_SERVER_DATA));
CrawlerOverviewLogic.actions.fetchCrawlerData();
await nextTick();
expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/crawler');
- expect(CrawlerOverviewLogic.actions.onFetchCrawlerData).toHaveBeenCalledWith({
- domains: [
- {
- id: '507f1f77bcf86cd799439011',
- createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000',
- url: 'moviedatabase.com',
- documentCount: 13,
- sitemaps: [],
- entryPoints: [],
- crawlRules: [],
- },
- ],
- });
+ expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith(
+ MOCK_CLIENT_DATA
+ );
});
it('calls flashApiErrors when there is an error', async () => {
@@ -121,5 +120,34 @@ describe('CrawlerOverviewLogic', () => {
expect(flashAPIErrors).toHaveBeenCalledWith('error');
});
});
+
+ describe('deleteDomain', () => {
+ it('calls onReceiveCrawlerData with retrieved data that has been converted from server to client', async () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'onReceiveCrawlerData');
+
+ http.delete.mockReturnValue(Promise.resolve(MOCK_SERVER_DATA));
+ CrawlerOverviewLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain);
+ await nextTick();
+
+ expect(http.delete).toHaveBeenCalledWith(
+ '/api/app_search/engines/some-engine/crawler/domains/1234',
+ {
+ query: { respond_with: 'crawler_details' },
+ }
+ );
+ expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith(
+ MOCK_CLIENT_DATA
+ );
+ expect(setSuccessMessage).toHaveBeenCalled();
+ });
+
+ it('calls flashApiErrors when there is an error', async () => {
+ http.delete.mockReturnValue(Promise.reject('error'));
+ CrawlerOverviewLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain);
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('error');
+ });
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts
index 6f04ade5962eb..dceb4e205487d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts
@@ -7,22 +7,36 @@
import { kea, MakeLogicType } from 'kea';
-import { flashAPIErrors } from '../../../shared/flash_messages';
+import { i18n } from '@kbn/i18n';
+
+import { flashAPIErrors, setSuccessMessage } from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
import { EngineLogic } from '../engine';
-import { CrawlerData, CrawlerDataFromServer, CrawlerDomain } from './types';
+import { CrawlerData, CrawlerDomain } from './types';
import { crawlerDataServerToClient } from './utils';
+export const DELETE_DOMAIN_MESSAGE = (domainUrl: string) =>
+ i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.delete.successMessage',
+ {
+ defaultMessage: 'Successfully deleted "{domainUrl}"',
+ values: {
+ domainUrl,
+ },
+ }
+ );
+
interface CrawlerOverviewValues {
dataLoading: boolean;
domains: CrawlerDomain[];
}
interface CrawlerOverviewActions {
+ deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain };
fetchCrawlerData(): void;
- onFetchCrawlerData(data: CrawlerData): { data: CrawlerData };
+ onReceiveCrawlerData(data: CrawlerData): { data: CrawlerData };
}
export const CrawlerOverviewLogic = kea<
@@ -30,20 +44,21 @@ export const CrawlerOverviewLogic = kea<
>({
path: ['enterprise_search', 'app_search', 'crawler', 'crawler_overview'],
actions: {
+ deleteDomain: (domain) => ({ domain }),
fetchCrawlerData: true,
- onFetchCrawlerData: (data) => ({ data }),
+ onReceiveCrawlerData: (data) => ({ data }),
},
reducers: {
dataLoading: [
true,
{
- onFetchCrawlerData: () => false,
+ onReceiveCrawlerData: () => false,
},
],
domains: [
[],
{
- onFetchCrawlerData: (_, { data: { domains } }) => domains,
+ onReceiveCrawlerData: (_, { data: { domains } }) => domains,
},
],
},
@@ -54,8 +69,28 @@ export const CrawlerOverviewLogic = kea<
try {
const response = await http.get(`/api/app_search/engines/${engineName}/crawler`);
- const crawlerData = crawlerDataServerToClient(response as CrawlerDataFromServer);
- actions.onFetchCrawlerData(crawlerData);
+ const crawlerData = crawlerDataServerToClient(response);
+ actions.onReceiveCrawlerData(crawlerData);
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
+ deleteDomain: async ({ domain }) => {
+ const { http } = HttpLogic.values;
+ const { engineName } = EngineLogic.values;
+
+ try {
+ const response = await http.delete(
+ `/api/app_search/engines/${engineName}/crawler/domains/${domain.id}`,
+ {
+ query: {
+ respond_with: 'crawler_details',
+ },
+ }
+ );
+ const crawlerData = crawlerDataServerToClient(response);
+ actions.onReceiveCrawlerData(crawlerData);
+ setSuccessMessage(DELETE_DOMAIN_MESSAGE(domain.url));
} catch (e) {
flashAPIErrors(e);
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
index 87e6ee62460fa..870e303a2930d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
@@ -16,18 +16,13 @@ import { engines } from '../../__mocks__/engines.mock';
import { nextTick } from '@kbn/test/jest';
import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { RoleMappingsLogic } from './role_mappings_logic';
describe('RoleMappingsLogic', () => {
const { http } = mockHttpValues;
- const {
- clearFlashMessages,
- flashAPIErrors,
- setSuccessMessage,
- setErrorMessage,
- } = mockFlashMessageHelpers;
+ const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers;
const { mount } = new LogicMounter(RoleMappingsLogic);
const DEFAULT_VALUES = {
attributes: [],
@@ -50,15 +45,14 @@ describe('RoleMappingsLogic', () => {
roleMappingErrors: [],
};
- const mappingsServerProps = { multipleAuthProvidersConfig: true, roleMappings: [asRoleMapping] };
- const mappingServerProps = {
+ const mappingsServerProps = {
+ multipleAuthProvidersConfig: true,
+ roleMappings: [asRoleMapping],
attributes: ['email', 'metadata', 'username', 'role'],
authProviders: [ANY_AUTH_PROVIDER],
availableEngines: engines,
elasticsearchRoles: [],
hasAdvancedRoles: false,
- multipleAuthProvidersConfig: false,
- roleMapping: asRoleMapping,
};
beforeEach(() => {
@@ -75,48 +69,20 @@ describe('RoleMappingsLogic', () => {
it('sets data based on server response from the `mappings` (plural) endpoint', () => {
RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
- expect(RoleMappingsLogic.values.roleMappings).toEqual([asRoleMapping]);
- expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
- expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true);
- });
- });
-
- describe('setRoleMappingData', () => {
- it('sets state based on server response from the `mapping` (singular) endpoint', () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
-
expect(RoleMappingsLogic.values).toEqual({
...DEFAULT_VALUES,
- roleMapping: asRoleMapping,
+ roleMappings: [asRoleMapping],
dataLoading: false,
- attributes: mappingServerProps.attributes,
- availableAuthProviders: mappingServerProps.authProviders,
- availableEngines: mappingServerProps.availableEngines,
+ attributes: mappingsServerProps.attributes,
+ availableAuthProviders: mappingsServerProps.authProviders,
+ availableEngines: mappingsServerProps.availableEngines,
accessAllEngines: true,
- attributeName: 'role',
- attributeValue: 'superuser',
- elasticsearchRoles: mappingServerProps.elasticsearchRoles,
- selectedEngines: new Set(engines.map((e) => e.name)),
- selectedOptions: [
- { label: engines[0].name, value: engines[0].name },
- { label: engines[1].name, value: engines[1].name },
- ],
- });
- });
-
- it('will remove all selected engines if no roleMapping was returned from the server', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: undefined,
- });
-
- expect(RoleMappingsLogic.values).toEqual({
- ...DEFAULT_VALUES,
- dataLoading: false,
+ multipleAuthProvidersConfig: true,
+ attributeName: 'username',
+ attributeValue: '',
+ elasticsearchRoles: mappingsServerProps.elasticsearchRoles,
selectedEngines: new Set(),
- attributes: mappingServerProps.attributes,
- availableAuthProviders: mappingServerProps.authProviders,
- availableEngines: mappingServerProps.availableEngines,
+ selectedOptions: [],
});
});
});
@@ -135,11 +101,13 @@ describe('RoleMappingsLogic', () => {
const engine = engines[0];
const otherEngine = engines[1];
const mountedValues = {
- ...mappingServerProps,
- roleMapping: {
- ...asRoleMapping,
- engines: [engine, otherEngine],
- },
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...asRoleMapping,
+ engines: [engine, otherEngine],
+ },
+ ],
selectedEngines: new Set([engine.name]),
};
@@ -153,11 +121,18 @@ describe('RoleMappingsLogic', () => {
expect(RoleMappingsLogic.values.selectedEngines).toEqual(
new Set([engine.name, otherEngine.name])
);
+ expect(RoleMappingsLogic.values.selectedOptions).toEqual([
+ { label: engine.name, value: engine.name },
+ { label: otherEngine.name, value: otherEngine.name },
+ ]);
});
it('handles removing an engine from selected engines', () => {
RoleMappingsLogic.actions.handleEngineSelectionChange([engine.name]);
expect(RoleMappingsLogic.values.selectedEngines).toEqual(new Set([engine.name]));
+ expect(RoleMappingsLogic.values.selectedOptions).toEqual([
+ { label: engine.name, value: engine.name },
+ ]);
});
});
@@ -175,17 +150,19 @@ describe('RoleMappingsLogic', () => {
it('sets values correctly', () => {
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
elasticsearchRoles,
});
RoleMappingsLogic.actions.handleAttributeSelectorChange('role', elasticsearchRoles[0]);
expect(RoleMappingsLogic.values).toEqual({
...DEFAULT_VALUES,
+ multipleAuthProvidersConfig: true,
attributeValue: elasticsearchRoles[0],
- roleMapping: asRoleMapping,
- attributes: mappingServerProps.attributes,
- availableEngines: mappingServerProps.availableEngines,
+ roleMappings: [asRoleMapping],
+ roleMapping: null,
+ attributes: mappingsServerProps.attributes,
+ availableEngines: mappingsServerProps.availableEngines,
accessAllEngines: true,
attributeName: 'role',
elasticsearchRoles,
@@ -215,11 +192,13 @@ describe('RoleMappingsLogic', () => {
describe('handleAuthProviderChange', () => {
beforeEach(() => {
mount({
- ...mappingServerProps,
- roleMapping: {
- ...asRoleMapping,
- authProvider: ['foo'],
- },
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...asRoleMapping,
+ authProvider: ['foo'],
+ },
+ ],
});
});
const providers = ['bar', 'baz'];
@@ -244,11 +223,13 @@ describe('RoleMappingsLogic', () => {
it('handles "any" auth in previous state', () => {
mount({
- ...mappingServerProps,
- roleMapping: {
- ...asRoleMapping,
- authProvider: [ANY_AUTH_PROVIDER],
- },
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...asRoleMapping,
+ authProvider: [ANY_AUTH_PROVIDER],
+ },
+ ],
});
RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny);
@@ -258,7 +239,6 @@ describe('RoleMappingsLogic', () => {
it('resetState', () => {
mount(mappingsServerProps);
- mount(mappingServerProps);
RoleMappingsLogic.actions.resetState();
expect(RoleMappingsLogic.values).toEqual(DEFAULT_VALUES);
@@ -266,7 +246,7 @@ describe('RoleMappingsLogic', () => {
});
it('openRoleMappingFlyout', () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
RoleMappingsLogic.actions.openRoleMappingFlyout();
expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(true);
@@ -275,7 +255,7 @@ describe('RoleMappingsLogic', () => {
it('closeRoleMappingFlyout', () => {
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
roleMappingFlyoutOpen: true,
});
RoleMappingsLogic.actions.closeRoleMappingFlyout();
@@ -307,40 +287,20 @@ describe('RoleMappingsLogic', () => {
});
describe('initializeRoleMapping', () => {
- it('calls API and sets values for new mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping();
-
- expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings/new');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('calls API and sets values for existing mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping('123');
-
- expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings/123');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('handles error', async () => {
- http.get.mockReturnValue(Promise.reject('this is an error'));
- RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
+ it('sets values for existing mapping', () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
+ RoleMappingsLogic.actions.initializeRoleMapping(asRoleMapping.id);
- expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
+ expect(setRoleMappingDataSpy).toHaveBeenCalledWith(asRoleMapping);
});
- it('shows error when there is a 404 status', async () => {
- http.get.mockReturnValue(Promise.reject({ status: 404 }));
+ it('does not set data for new mapping', async () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
- expect(setErrorMessage).toHaveBeenCalledWith(ROLE_MAPPING_NOT_FOUND);
+ expect(setRoleMappingDataSpy).not.toHaveBeenCalledWith(asRoleMapping);
});
});
@@ -362,7 +322,7 @@ describe('RoleMappingsLogic', () => {
'initializeRoleMappings'
);
- http.post.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.post.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.post).toHaveBeenCalledWith('/api/app_search/role_mappings', {
@@ -374,13 +334,16 @@ describe('RoleMappingsLogic', () => {
});
it('calls API and refreshes list when existing mapping', async () => {
- mount(mappingServerProps);
+ mount({
+ ...mappingsServerProps,
+ roleMapping: asRoleMapping,
+ });
const initializeRoleMappingsSpy = jest.spyOn(
RoleMappingsLogic.actions,
'initializeRoleMappings'
);
- http.put.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.put.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.put).toHaveBeenCalledWith(`/api/app_search/role_mappings/${asRoleMapping.id}`, {
@@ -396,12 +359,13 @@ describe('RoleMappingsLogic', () => {
const engine = engines[0];
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
accessAllEngines: false,
selectedEngines: new Set([engine.name]),
+ roleMapping: asRoleMapping,
});
- http.put.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.put.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.put).toHaveBeenCalledWith(`/api/app_search/role_mappings/${asRoleMapping.id}`, {
@@ -449,7 +413,7 @@ describe('RoleMappingsLogic', () => {
});
it('calls API and refreshes list', async () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
const initializeRoleMappingsSpy = jest.spyOn(
RoleMappingsLogic.actions,
'initializeRoleMappings'
@@ -465,7 +429,7 @@ describe('RoleMappingsLogic', () => {
});
it('handles error', async () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
http.delete.mockReturnValue(Promise.reject('this is an error'));
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
await nextTick();
@@ -474,7 +438,7 @@ describe('RoleMappingsLogic', () => {
});
it('will do nothing if not confirmed', () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
jest.spyOn(window, 'confirm').mockReturnValueOnce(false);
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
index 863e6746dbe95..fc0a235b23c77 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
@@ -13,10 +13,9 @@ import {
clearFlashMessages,
flashAPIErrors,
setSuccessMessage,
- setErrorMessage,
} from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { AttributeName } from '../../../shared/types';
import { ASRoleMapping, RoleTypes } from '../../types';
import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines';
@@ -31,17 +30,12 @@ import {
interface RoleMappingsServerDetails {
roleMappings: ASRoleMapping[];
- multipleAuthProvidersConfig: boolean;
-}
-
-interface RoleMappingServerDetails {
attributes: string[];
authProviders: string[];
availableEngines: Engine[];
elasticsearchRoles: string[];
hasAdvancedRoles: boolean;
multipleAuthProvidersConfig: boolean;
- roleMapping?: ASRoleMapping;
}
const getFirstAttributeName = (roleMapping: ASRoleMapping) =>
@@ -64,7 +58,7 @@ interface RoleMappingsActions {
initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string };
initializeRoleMappings(): void;
resetState(): void;
- setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails;
+ setRoleMapping(roleMapping: ASRoleMapping): { roleMapping: ASRoleMapping };
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
openRoleMappingFlyout(): void;
closeRoleMappingFlyout(): void;
@@ -96,7 +90,7 @@ export const RoleMappingsLogic = kea data,
- setRoleMappingData: (data: RoleMappingServerDetails) => data,
+ setRoleMapping: (roleMapping: ASRoleMapping) => ({ roleMapping }),
setRoleMappingErrors: (errors: string[]) => ({ errors }),
handleAuthProviderChange: (value: string) => ({ value }),
handleRoleChange: (roleType: RoleTypes) => ({ roleType }),
@@ -120,7 +114,6 @@ export const RoleMappingsLogic = kea false,
- setRoleMappingData: () => false,
resetState: () => true,
},
],
@@ -135,40 +128,39 @@ export const RoleMappingsLogic = kea multipleAuthProvidersConfig,
- setRoleMappingData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig,
resetState: () => false,
},
],
hasAdvancedRoles: [
false,
{
- setRoleMappingData: (_, { hasAdvancedRoles }) => hasAdvancedRoles,
+ setRoleMappingsData: (_, { hasAdvancedRoles }) => hasAdvancedRoles,
},
],
availableEngines: [
[],
{
- setRoleMappingData: (_, { availableEngines }) => availableEngines,
+ setRoleMappingsData: (_, { availableEngines }) => availableEngines,
resetState: () => [],
},
],
attributes: [
[],
{
- setRoleMappingData: (_, { attributes }) => attributes,
+ setRoleMappingsData: (_, { attributes }) => attributes,
resetState: () => [],
},
],
elasticsearchRoles: [
[],
{
- setRoleMappingData: (_, { elasticsearchRoles }) => elasticsearchRoles,
+ setRoleMappingsData: (_, { elasticsearchRoles }) => elasticsearchRoles,
},
],
roleMapping: [
null,
{
- setRoleMappingData: (_, { roleMapping }) => roleMapping || null,
+ setRoleMapping: (_, { roleMapping }) => roleMapping,
resetState: () => null,
closeRoleMappingFlyout: () => null,
},
@@ -176,16 +168,14 @@ export const RoleMappingsLogic = kea
- roleMapping ? (roleMapping.roleType as RoleTypes) : 'owner',
+ setRoleMapping: (_, { roleMapping }) => roleMapping.roleType as RoleTypes,
handleRoleChange: (_, { roleType }) => roleType,
},
],
accessAllEngines: [
true,
{
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? roleMapping.accessAllEngines : true,
+ setRoleMapping: (_, { roleMapping }) => roleMapping.accessAllEngines,
handleRoleChange: (_, { roleType }) => !roleHasScopedEngines(roleType),
handleAccessAllEnginesChange: (_, { selected }) => selected,
},
@@ -193,8 +183,7 @@ export const RoleMappingsLogic = kea
- roleMapping ? getFirstAttributeValue(roleMapping) : '',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeValue(roleMapping),
handleAttributeSelectorChange: (_, { value, firstElasticsearchRole }) =>
value === 'role' ? firstElasticsearchRole : '',
handleAttributeValueChange: (_, { value }) => value,
@@ -205,8 +194,7 @@ export const RoleMappingsLogic = kea
- roleMapping ? getFirstAttributeName(roleMapping) : 'username',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeName(roleMapping),
handleAttributeSelectorChange: (_, { value }) => value,
resetState: () => 'username',
closeRoleMappingFlyout: () => 'username',
@@ -215,8 +203,8 @@ export const RoleMappingsLogic = kea
- roleMapping ? new Set(roleMapping.engines.map((engine) => engine.name)) : new Set(),
+ setRoleMapping: (_, { roleMapping }) =>
+ new Set(roleMapping.engines.map((engine: Engine) => engine.name)),
handleAccessAllEnginesChange: () => new Set(),
handleEngineSelectionChange: (_, { engineNames }) => {
const newSelectedEngineNames = new Set() as Set;
@@ -229,7 +217,7 @@ export const RoleMappingsLogic = kea authProviders,
+ setRoleMappingsData: (_, { authProviders }) => authProviders,
},
],
selectedAuthProviders: [
@@ -246,8 +234,7 @@ export const RoleMappingsLogic = kea v !== ANY_AUTH_PROVIDER);
return [ANY_AUTH_PROVIDER];
},
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? roleMapping.authProvider : [ANY_AUTH_PROVIDER],
+ setRoleMapping: (_, { roleMapping }) => roleMapping.authProvider,
},
],
roleMappingFlyoutOpen: [
@@ -292,21 +279,8 @@ export const RoleMappingsLogic = kea {
- const { http } = HttpLogic.values;
- const route = roleMappingId
- ? `/api/app_search/role_mappings/${roleMappingId}`
- : '/api/app_search/role_mappings/new';
-
- try {
- const response = await http.get(route);
- actions.setRoleMappingData(response);
- } catch (e) {
- if (e.status === 404) {
- setErrorMessage(ROLE_MAPPING_NOT_FOUND);
- } else {
- flashAPIErrors(e);
- }
- }
+ const roleMapping = values.roleMappings.find(({ id }) => id === roleMappingId);
+ if (roleMapping) actions.setRoleMapping(roleMapping);
},
handleDeleteMapping: async ({ roleMappingId }) => {
const { http } = HttpLogic.values;
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
index c8fb009fb31da..bd5bdb7b2f665 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
@@ -37,7 +37,7 @@ export const ENGINE_SCHEMA_PATH = `${ENGINE_PATH}/schema`;
export const ENGINE_REINDEX_JOB_PATH = `${ENGINE_SCHEMA_PATH}/reindex_job/:reindexJobId`;
export const ENGINE_CRAWLER_PATH = `${ENGINE_PATH}/crawler`;
-// TODO: Crawler sub-pages
+export const ENGINE_CRAWLER_DOMAIN_PATH = `${ENGINE_CRAWLER_PATH}/domains/:domainId`;
export const META_ENGINE_CREATION_PATH = '/meta_engine_creation';
export const META_ENGINE_SOURCE_ENGINES_PATH = `${ENGINE_PATH}/engines`;
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
index 82284be0907fb..7696cf03ed4b1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
@@ -7,7 +7,7 @@
import React, { Fragment } from 'react';
-import { EuiTextColor, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
+import { EuiIconTip, EuiTextColor, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
import { ASRoleMapping } from '../../app_search/types';
import { WSRoleMapping } from '../../workplace_search/types';
@@ -84,7 +84,7 @@ export const RoleMappingsTable: React.FC = ({
const roleCol: EuiBasicTableColumn = {
field: 'roleType',
name: ROLE_LABEL,
- render: (_, { rules }: SharedRoleMapping) => getFirstAttributeValue(rules),
+ render: (_, { roleType }: SharedRoleMapping) => roleType,
};
const accessItemsCol: EuiBasicTableColumn = {
@@ -124,11 +124,16 @@ export const RoleMappingsTable: React.FC = ({
field: 'id',
name: '',
align: 'right',
- render: (_, { id }: SharedRoleMapping) => (
- initializeRoleMapping(id)}
- onDeleteClick={() => handleDeleteMapping(id)}
- />
+ render: (_, { id, toolTip }: SharedRoleMapping) => (
+ <>
+ {id && (
+ initializeRoleMapping(id)}
+ onDeleteClick={() => handleDeleteMapping(id)}
+ />
+ )}
+ {toolTip && }
+ >
),
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts
index b9a49c416f283..e1c2a3b76e3ff 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts
@@ -8,3 +8,5 @@
export { WorkplaceSearchNav } from './nav';
export { WorkplaceSearchHeaderActions } from './kibana_header_actions';
export { AccountHeader } from './account_header';
+export { PersonalDashboardLayout } from './personal_dashboard_layout';
+export { PrivateSourcesSidebar, AccountSettingsSidebar } from './personal_dashboard_sidebar';
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/index.ts
new file mode 100644
index 0000000000000..40347aaee747d
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { PersonalDashboardLayout } from './personal_dashboard_layout';
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss
new file mode 100644
index 0000000000000..175f6b9ebca20
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+.personalDashboardLayout {
+ $sideBarWidth: $euiSize * 30;
+ $consoleHeaderHeight: 48px; // NOTE: Keep an eye on this for changes
+ $pageHeight: calc(100vh - #{$consoleHeaderHeight});
+
+ left: $sideBarWidth;
+ width: calc(100% - #{$sideBarWidth});
+ min-height: $pageHeight;
+
+ &__sideBar {
+ padding: 32px 40px 40px;
+ width: $sideBarWidth;
+ margin-left: -$sideBarWidth;
+ height: $pageHeight;
+ }
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx
new file mode 100644
index 0000000000000..faeaa7323e93f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiCallOut } from '@elastic/eui';
+
+import { AccountHeader } from '..';
+
+import { PersonalDashboardLayout } from './personal_dashboard_layout';
+
+describe('PersonalDashboardLayout', () => {
+ const children = test
;
+ const sidebar = test
;
+
+ it('renders', () => {
+ const wrapper = shallow(
+ {children}
+ );
+
+ expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1);
+ expect(wrapper.find('[data-test-subj="TestSidebar"]')).toHaveLength(1);
+ expect(wrapper.find(AccountHeader)).toHaveLength(1);
+ });
+
+ it('renders callout when in read-only mode', () => {
+ const wrapper = shallow(
+
+ {children}
+
+ );
+
+ expect(wrapper.find(EuiCallOut)).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx
new file mode 100644
index 0000000000000..1ab9e07dfa14d
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui';
+
+import { AccountHeader } from '..';
+
+import { PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING } from '../../../views/content_sources/constants';
+
+import './personal_dashboard_layout.scss';
+
+interface LayoutProps {
+ restrictWidth?: boolean;
+ readOnlyMode?: boolean;
+ sidebar: React.ReactNode;
+}
+
+export const PersonalDashboardLayout: React.FC = ({
+ children,
+ restrictWidth,
+ readOnlyMode,
+ sidebar,
+}) => {
+ return (
+ <>
+
+
+
+ {sidebar}
+
+
+ {readOnlyMode && (
+
+ )}
+ {children}
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.test.tsx
new file mode 100644
index 0000000000000..8edcf83a57e6f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.test.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { ViewContentHeader } from '../../shared/view_content_header';
+
+import { AccountSettingsSidebar } from './account_settings_sidebar';
+
+describe('AccountSettingsSidebar', () => {
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.tsx
new file mode 100644
index 0000000000000..490f3ff0ae4a5
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.tsx
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { ACCOUNT_SETTINGS_TITLE, ACCOUNT_SETTINGS_DESCRIPTION } from '../../../constants';
+import { ViewContentHeader } from '../../shared/view_content_header';
+
+export const AccountSettingsSidebar = () => {
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/index.ts
new file mode 100644
index 0000000000000..ffd241000c8a0
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { PrivateSourcesSidebar } from './private_sources_sidebar';
+export { AccountSettingsSidebar } from './account_settings_sidebar';
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
similarity index 54%
rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx
rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
index 6af439814b891..387724af970f8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
@@ -5,50 +5,42 @@
* 2.0.
*/
-import '../../../__mocks__/shallow_useeffect.mock';
-
-import { setMockValues } from '../../../__mocks__/kea_logic';
+import { setMockValues } from '../../../../__mocks__/kea_logic';
import React from 'react';
import { shallow } from 'enzyme';
-import { EuiCallOut } from '@elastic/eui';
-
-import { AccountHeader } from '../../components/layout';
-import { ViewContentHeader } from '../../components/shared/view_content_header';
-
-import { SourceSubNav } from './components/source_sub_nav';
-
import {
PRIVATE_CAN_CREATE_PAGE_TITLE,
PRIVATE_VIEW_ONLY_PAGE_TITLE,
PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION,
PRIVATE_CAN_CREATE_PAGE_DESCRIPTION,
-} from './constants';
-import { PrivateSourcesLayout } from './private_sources_layout';
+} from '../../../constants';
+import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav';
+
+import { ViewContentHeader } from '../../shared/view_content_header';
-describe('PrivateSourcesLayout', () => {
+import { PrivateSourcesSidebar } from './private_sources_sidebar';
+
+describe('PrivateSourcesSidebar', () => {
const mockValues = {
account: { canCreatePersonalSources: true },
};
- const children = test
;
-
beforeEach(() => {
setMockValues({ ...mockValues });
});
it('renders', () => {
- const wrapper = shallow({children} );
+ const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1);
+ expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
expect(wrapper.find(SourceSubNav)).toHaveLength(1);
- expect(wrapper.find(AccountHeader)).toHaveLength(1);
});
it('uses correct title and description when private sources are enabled', () => {
- const wrapper = shallow({children} );
+ const wrapper = shallow( );
expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_CAN_CREATE_PAGE_TITLE);
expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
@@ -58,17 +50,11 @@ describe('PrivateSourcesLayout', () => {
it('uses correct title and description when private sources are disabled', () => {
setMockValues({ account: { canCreatePersonalSources: false } });
- const wrapper = shallow({children} );
+ const wrapper = shallow( );
expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_VIEW_ONLY_PAGE_TITLE);
expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION
);
});
-
- it('renders callout when in read-only mode', () => {
- const wrapper = shallow({children} );
-
- expect(wrapper.find(EuiCallOut)).toHaveLength(1);
- });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
new file mode 100644
index 0000000000000..5505ae57b2ad5
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { useValues } from 'kea';
+
+import { AppLogic } from '../../../app_logic';
+import {
+ PRIVATE_CAN_CREATE_PAGE_TITLE,
+ PRIVATE_VIEW_ONLY_PAGE_TITLE,
+ PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION,
+ PRIVATE_CAN_CREATE_PAGE_DESCRIPTION,
+} from '../../../constants';
+import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav';
+import { ViewContentHeader } from '../../shared/view_content_header';
+
+export const PrivateSourcesSidebar = () => {
+ const {
+ account: { canCreatePersonalSources },
+ } = useValues(AppLogic);
+
+ const PAGE_TITLE = canCreatePersonalSources
+ ? PRIVATE_CAN_CREATE_PAGE_TITLE
+ : PRIVATE_VIEW_ONLY_PAGE_TITLE;
+ const PAGE_DESCRIPTION = canCreatePersonalSources
+ ? PRIVATE_CAN_CREATE_PAGE_DESCRIPTION
+ : PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION;
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
index dcebc35d45f71..aa5419f12c7f3 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
@@ -662,6 +662,49 @@ export const PRIVATE_SOURCES = i18n.translate(
}
);
+export const PRIVATE_CAN_CREATE_PAGE_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.private.canCreate.title',
+ {
+ defaultMessage: 'Manage private content sources',
+ }
+);
+
+export const PRIVATE_VIEW_ONLY_PAGE_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.private.vewOnly.title',
+ {
+ defaultMessage: 'Review Group Sources',
+ }
+);
+
+export const PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.private.vewOnly.description',
+ {
+ defaultMessage: 'Review the status of all sources shared with your Group.',
+ }
+);
+
+export const PRIVATE_CAN_CREATE_PAGE_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.private.canCreate.description',
+ {
+ defaultMessage:
+ 'Review the status of all connected private sources, and manage private sources for your account.',
+ }
+);
+
+export const ACCOUNT_SETTINGS_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.accountSettings.title',
+ {
+ defaultMessage: 'Account Settings',
+ }
+);
+
+export const ACCOUNT_SETTINGS_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.accountSettings.description',
+ {
+ defaultMessage: 'Manage access, passwords, and other account settings.',
+ }
+);
+
export const CONFIRM_CHANGES_TEXT = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.confirmChanges.text',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
index 0fc8a6e7c7c0d..7e911b31c516b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
@@ -19,6 +19,11 @@ import { NotFound } from '../shared/not_found';
import { AppLogic } from './app_logic';
import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout';
+import {
+ PersonalDashboardLayout,
+ PrivateSourcesSidebar,
+ AccountSettingsSidebar,
+} from './components/layout';
import {
GROUPS_PATH,
SETUP_GUIDE_PATH,
@@ -34,7 +39,6 @@ import { AccountSettings } from './views/account_settings';
import { SourcesRouter } from './views/content_sources';
import { SourceAdded } from './views/content_sources/components/source_added';
import { SourceSubNav } from './views/content_sources/components/source_sub_nav';
-import { PrivateSourcesLayout } from './views/content_sources/private_sources_layout';
import { ErrorState } from './views/error_state';
import { GroupsRouter } from './views/groups';
import { GroupSubNav } from './views/groups/components/group_sub_nav';
@@ -101,14 +105,22 @@ export const WorkplaceSearchConfigured: React.FC = (props) => {
)}
-
+ }
+ >
-
+
-
+ }
+ >
-
+
= ({
- children,
- restrictWidth,
- readOnlyMode,
-}) => {
- const {
- account: { canCreatePersonalSources },
- } = useValues(AppLogic);
-
- const PAGE_TITLE = canCreatePersonalSources
- ? PRIVATE_CAN_CREATE_PAGE_TITLE
- : PRIVATE_VIEW_ONLY_PAGE_TITLE;
- const PAGE_DESCRIPTION = canCreatePersonalSources
- ? PRIVATE_CAN_CREATE_PAGE_DESCRIPTION
- : PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION;
-
- return (
- <>
-
-
-
-
-
-
-
- {readOnlyMode && (
-
- )}
- {children}
-
-
- >
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss
index 549ca3ae9154e..feccc0e1924d2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss
@@ -18,23 +18,6 @@
}
}
-.privateSourcesLayout {
- $sideBarWidth: $euiSize * 30;
- $consoleHeaderHeight: 48px; // NOTE: Keep an eye on this for changes
- $pageHeight: calc(100vh - #{$consoleHeaderHeight});
-
- left: $sideBarWidth;
- width: calc(100% - #{$sideBarWidth});
- min-height: $pageHeight;
-
- &__sideBar {
- padding: 32px 40px 40px;
- width: $sideBarWidth;
- margin-left: -$sideBarWidth;
- height: $pageHeight;
- }
-}
-
.sourcesSubNav {
li {
display: block;
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
index 716cb8ebb6d47..4ee530870284e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
@@ -16,13 +16,13 @@ import { groups } from '../../__mocks__/groups.mock';
import { nextTick } from '@kbn/test/jest';
import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { RoleMappingsLogic } from './role_mappings_logic';
describe('RoleMappingsLogic', () => {
const { http } = mockHttpValues;
- const { clearFlashMessages, flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers;
+ const { clearFlashMessages, flashAPIErrors } = mockFlashMessageHelpers;
const { mount } = new LogicMounter(RoleMappingsLogic);
const defaultValues = {
attributes: [],
@@ -52,14 +52,13 @@ describe('RoleMappingsLogic', () => {
name: 'Default',
};
- const mappingsServerProps = { multipleAuthProvidersConfig: true, roleMappings: [wsRoleMapping] };
- const mappingServerProps = {
+ const mappingsServerProps = {
+ multipleAuthProvidersConfig: true,
+ roleMappings: [wsRoleMapping],
attributes: [],
authProviders: [],
availableGroups: [roleGroup, defaultGroup],
elasticsearchRoles: [],
- multipleAuthProvidersConfig: false,
- roleMapping: wsRoleMapping,
};
beforeEach(() => {
@@ -78,36 +77,17 @@ describe('RoleMappingsLogic', () => {
expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]);
expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true);
- });
-
- describe('setRoleMappingData', () => {
- it('sets data correctly', () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
-
- expect(RoleMappingsLogic.values.roleMapping).toEqual(wsRoleMapping);
- expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
- expect(RoleMappingsLogic.values.attributes).toEqual(mappingServerProps.attributes);
- expect(RoleMappingsLogic.values.availableGroups).toEqual(
- mappingServerProps.availableGroups
- );
- expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(true);
- expect(RoleMappingsLogic.values.elasticsearchRoles).toEqual(
- mappingServerProps.elasticsearchRoles
- );
- expect(RoleMappingsLogic.values.selectedGroups).toEqual(
- new Set([wsRoleMapping.groups[0].id])
- );
- expect(RoleMappingsLogic.values.selectedOptions).toEqual([]);
- });
-
- it('sets default group with new role mapping', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: undefined,
- });
-
- expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id]));
- });
+ expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
+ expect(RoleMappingsLogic.values.attributes).toEqual(mappingsServerProps.attributes);
+ expect(RoleMappingsLogic.values.availableGroups).toEqual(mappingsServerProps.availableGroups);
+ expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(false);
+ expect(RoleMappingsLogic.values.elasticsearchRoles).toEqual(
+ mappingsServerProps.elasticsearchRoles
+ );
+ expect(RoleMappingsLogic.values.selectedOptions).toEqual([
+ { label: defaultGroup.name, value: defaultGroup.id },
+ ]);
+ expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id]));
});
it('handleRoleChange', () => {
@@ -119,14 +99,17 @@ describe('RoleMappingsLogic', () => {
it('handleGroupSelectionChange', () => {
const group = wsRoleMapping.groups[0];
const otherGroup = groups[0];
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: {
- ...wsRoleMapping,
- groups: [group, otherGroup],
- },
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...wsRoleMapping,
+ groups: [group, otherGroup],
+ },
+ ],
});
+ RoleMappingsLogic.actions.initializeRoleMapping(wsRoleMapping.id);
RoleMappingsLogic.actions.handleGroupSelectionChange([group.id, otherGroup.id]);
expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([group.id, otherGroup.id]));
expect(RoleMappingsLogic.values.selectedOptions).toEqual([
@@ -147,8 +130,8 @@ describe('RoleMappingsLogic', () => {
const elasticsearchRoles = ['foo', 'bar'];
it('sets values correctly', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
elasticsearchRoles,
});
RoleMappingsLogic.actions.handleAttributeSelectorChange('role', elasticsearchRoles[0]);
@@ -172,12 +155,14 @@ describe('RoleMappingsLogic', () => {
describe('handleAuthProviderChange', () => {
beforeEach(() => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: {
- ...wsRoleMapping,
- authProvider: ['foo'],
- },
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...wsRoleMapping,
+ authProvider: ['foo'],
+ },
+ ],
});
});
const providers = ['bar', 'baz'];
@@ -201,28 +186,23 @@ describe('RoleMappingsLogic', () => {
});
it('handles "any" auth in previous state', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: {
- ...wsRoleMapping,
- authProvider: [ANY_AUTH_PROVIDER],
- },
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...wsRoleMapping,
+ authProvider: [ANY_AUTH_PROVIDER],
+ },
+ ],
});
RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny);
expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([providers[1]]);
});
-
- it('handles catch-all state', () => {
- RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny);
-
- expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([ANY_AUTH_PROVIDER]);
- });
});
it('resetState', () => {
RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
RoleMappingsLogic.actions.resetState();
expect(RoleMappingsLogic.values.dataLoading).toEqual(true);
@@ -234,7 +214,7 @@ describe('RoleMappingsLogic', () => {
});
it('openRoleMappingFlyout', () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
RoleMappingsLogic.actions.openRoleMappingFlyout();
expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(true);
@@ -243,7 +223,7 @@ describe('RoleMappingsLogic', () => {
it('closeRoleMappingFlyout', () => {
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
roleMappingFlyoutOpen: true,
});
RoleMappingsLogic.actions.closeRoleMappingFlyout();
@@ -275,40 +255,20 @@ describe('RoleMappingsLogic', () => {
});
describe('initializeRoleMapping', () => {
- it('calls API and sets values for new mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping();
-
- expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings/new');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('calls API and sets values for existing mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping('123');
-
- expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings/123');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('handles error', async () => {
- http.get.mockReturnValue(Promise.reject('this is an error'));
- RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
+ it('sets values for existing mapping', () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
+ RoleMappingsLogic.actions.initializeRoleMapping(wsRoleMapping.id);
- expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
+ expect(setRoleMappingDataSpy).toHaveBeenCalledWith(wsRoleMapping);
});
- it('shows error when there is a 404 status', async () => {
- http.get.mockReturnValue(Promise.reject({ status: 404 }));
+ it('does not set data for new mapping', async () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
- expect(setErrorMessage).toHaveBeenCalledWith(ROLE_MAPPING_NOT_FOUND);
+ expect(setRoleMappingDataSpy).not.toHaveBeenCalledWith(wsRoleMapping);
});
});
@@ -320,7 +280,7 @@ describe('RoleMappingsLogic', () => {
);
RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
- http.post.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.post.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.post).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings', {
@@ -331,7 +291,7 @@ describe('RoleMappingsLogic', () => {
rules: {
username: '',
},
- groups: [],
+ groups: [defaultGroup.id],
}),
});
await nextTick();
@@ -344,9 +304,9 @@ describe('RoleMappingsLogic', () => {
RoleMappingsLogic.actions,
'initializeRoleMappings'
);
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
- http.put.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.put.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.put).toHaveBeenCalledWith(
@@ -408,7 +368,7 @@ describe('RoleMappingsLogic', () => {
RoleMappingsLogic.actions,
'initializeRoleMappings'
);
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
http.delete.mockReturnValue(Promise.resolve({}));
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
@@ -421,7 +381,7 @@ describe('RoleMappingsLogic', () => {
});
it('handles error', async () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
http.delete.mockReturnValue(Promise.reject('this is an error'));
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
await nextTick();
@@ -430,7 +390,7 @@ describe('RoleMappingsLogic', () => {
});
it('will do nothing if not confirmed', async () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
window.confirm = () => false;
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
index aee780ac18971..361425b7a78a1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
@@ -13,10 +13,9 @@ import {
clearFlashMessages,
flashAPIErrors,
setSuccessMessage,
- setErrorMessage,
} from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { AttributeName } from '../../../shared/types';
import { RoleGroup, WSRoleMapping, Role } from '../../types';
@@ -30,16 +29,11 @@ import {
interface RoleMappingsServerDetails {
roleMappings: WSRoleMapping[];
- multipleAuthProvidersConfig: boolean;
-}
-
-interface RoleMappingServerDetails {
attributes: string[];
authProviders: string[];
availableGroups: RoleGroup[];
elasticsearchRoles: string[];
multipleAuthProvidersConfig: boolean;
- roleMapping?: WSRoleMapping;
}
const getFirstAttributeName = (roleMapping: WSRoleMapping): AttributeName =>
@@ -62,7 +56,7 @@ interface RoleMappingsActions {
initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string };
initializeRoleMappings(): void;
resetState(): void;
- setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails;
+ setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping };
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
openRoleMappingFlyout(): void;
closeRoleMappingFlyout(): void;
@@ -93,7 +87,7 @@ export const RoleMappingsLogic = kea data,
- setRoleMappingData: (data: RoleMappingServerDetails) => data,
+ setRoleMapping: (roleMapping: WSRoleMapping) => ({ roleMapping }),
setRoleMappingErrors: (errors: string[]) => ({ errors }),
handleAuthProviderChange: (value: string[]) => ({ value }),
handleRoleChange: (roleType: Role) => ({ roleType }),
@@ -117,7 +111,6 @@ export const RoleMappingsLogic = kea false,
- setRoleMappingData: () => false,
resetState: () => true,
},
],
@@ -132,32 +125,31 @@ export const RoleMappingsLogic = kea multipleAuthProvidersConfig,
- setRoleMappingData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig,
resetState: () => false,
},
],
availableGroups: [
[],
{
- setRoleMappingData: (_, { availableGroups }) => availableGroups,
+ setRoleMappingsData: (_, { availableGroups }) => availableGroups,
},
],
attributes: [
[],
{
- setRoleMappingData: (_, { attributes }) => attributes,
+ setRoleMappingsData: (_, { attributes }) => attributes,
},
],
elasticsearchRoles: [
[],
{
- setRoleMappingData: (_, { elasticsearchRoles }) => elasticsearchRoles,
+ setRoleMappingsData: (_, { elasticsearchRoles }) => elasticsearchRoles,
},
],
roleMapping: [
null,
{
- setRoleMappingData: (_, { roleMapping }) => roleMapping || null,
+ setRoleMapping: (_, { roleMapping }) => roleMapping,
resetState: () => null,
closeRoleMappingFlyout: () => null,
},
@@ -165,23 +157,21 @@ export const RoleMappingsLogic = kea
- roleMapping ? (roleMapping.roleType as Role) : 'admin',
+ setRoleMapping: (_, { roleMapping }) => roleMapping.roleType as Role,
handleRoleChange: (_, { roleType }) => roleType,
},
],
includeInAllGroups: [
false,
{
- setRoleMappingData: (_, { roleMapping }) => (roleMapping ? roleMapping.allGroups : false),
+ setRoleMapping: (_, { roleMapping }) => roleMapping.allGroups,
handleAllGroupsSelectionChange: (_, { selected }) => selected,
},
],
attributeValue: [
'',
{
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? getFirstAttributeValue(roleMapping) : '',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeValue(roleMapping),
handleAttributeSelectorChange: (_, { value, firstElasticsearchRole }) =>
value === 'role' ? firstElasticsearchRole : '',
handleAttributeValueChange: (_, { value }) => value,
@@ -192,8 +182,7 @@ export const RoleMappingsLogic = kea
- roleMapping ? getFirstAttributeName(roleMapping) : 'username',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeName(roleMapping),
handleAttributeSelectorChange: (_, { value }) => value,
resetState: () => 'username',
closeRoleMappingFlyout: () => 'username',
@@ -202,14 +191,14 @@ export const RoleMappingsLogic = kea
- roleMapping
- ? new Set(roleMapping.groups.map((group) => group.id))
- : new Set(
- availableGroups
- .filter((group) => group.name === DEFAULT_GROUP_NAME)
- .map((group) => group.id)
- ),
+ setRoleMappingsData: (_, { availableGroups }) =>
+ new Set(
+ availableGroups
+ .filter((group) => group.name === DEFAULT_GROUP_NAME)
+ .map((group) => group.id)
+ ),
+ setRoleMapping: (_, { roleMapping }) =>
+ new Set(roleMapping.groups.map((group: RoleGroup) => group.id)),
handleGroupSelectionChange: (_, { groupIds }) => {
const newSelectedGroupNames = new Set() as Set;
groupIds.forEach((groupId) => newSelectedGroupNames.add(groupId));
@@ -221,7 +210,7 @@ export const RoleMappingsLogic = kea authProviders,
+ setRoleMappingsData: (_, { authProviders }) => authProviders,
},
],
selectedAuthProviders: [
@@ -230,15 +219,15 @@ export const RoleMappingsLogic = kea {
const previouslyContainedAny = previous.includes(ANY_AUTH_PROVIDER);
const newSelectionsContainAny = value.includes(ANY_AUTH_PROVIDER);
+ const hasItems = value.length > 0;
- if (value.length < 1) return [ANY_AUTH_PROVIDER];
if (value.length === 1) return value;
- if (!newSelectionsContainAny) return value;
- if (previouslyContainedAny) return value.filter((v) => v !== ANY_AUTH_PROVIDER);
+ if (!newSelectionsContainAny && hasItems) return value;
+ if (previouslyContainedAny && hasItems)
+ return value.filter((v) => v !== ANY_AUTH_PROVIDER);
return [ANY_AUTH_PROVIDER];
},
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? roleMapping.authProvider : [ANY_AUTH_PROVIDER],
+ setRoleMapping: (_, { roleMapping }) => roleMapping.authProvider,
},
],
roleMappingFlyoutOpen: [
@@ -283,21 +272,8 @@ export const RoleMappingsLogic = kea {
- const { http } = HttpLogic.values;
- const route = roleMappingId
- ? `/api/workplace_search/org/role_mappings/${roleMappingId}`
- : '/api/workplace_search/org/role_mappings/new';
-
- try {
- const response = await http.get(route);
- actions.setRoleMappingData(response);
- } catch (e) {
- if (e.status === 404) {
- setErrorMessage(ROLE_MAPPING_NOT_FOUND);
- } else {
- flashAPIErrors(e);
- }
- }
+ const roleMapping = values.roleMappings.find(({ id }) => id === roleMappingId);
+ if (roleMapping) actions.setRoleMapping(roleMapping);
},
handleDeleteMapping: async ({ roleMappingId }) => {
const { http } = HttpLogic.values;
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
index 626a107b6942b..06a206017fbd1 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
@@ -31,5 +31,61 @@ describe('crawler routes', () => {
path: '/api/as/v0/engines/:name/crawler',
});
});
+
+ it('validates correctly with name', () => {
+ const request = { params: { name: 'some-engine' } };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('fails validation without name', () => {
+ const request = { params: {} };
+ mockRouter.shouldThrow(request);
+ });
+ });
+
+ describe('DELETE /api/app_search/engines/{name}/crawler/domains/{id}', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'delete',
+ path: '/api/app_search/engines/{name}/crawler/domains/{id}',
+ });
+
+ registerCrawlerRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/api/as/v0/engines/:name/crawler/domains/:id',
+ });
+ });
+
+ it('validates correctly with name and id', () => {
+ const request = { params: { name: 'some-engine', id: '1234' } };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('fails validation without name', () => {
+ const request = { params: { id: '1234' } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('fails validation without id', () => {
+ const request = { params: { name: 'test-engine' } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('accepts a query param', () => {
+ const request = {
+ params: { name: 'test-engine', id: '1234' },
+ query: { respond_with: 'crawler_details' },
+ };
+ mockRouter.shouldValidate(request);
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
index 15b8340b07d4e..6c8ed7a49c64a 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
@@ -26,4 +26,22 @@ export function registerCrawlerRoutes({
path: '/api/as/v0/engines/:name/crawler',
})
);
+
+ router.delete(
+ {
+ path: '/api/app_search/engines/{name}/crawler/domains/{id}',
+ validate: {
+ params: schema.object({
+ name: schema.string(),
+ id: schema.string(),
+ }),
+ query: schema.object({
+ respond_with: schema.maybe(schema.string()),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/api/as/v0/engines/:name/crawler/domains/:id',
+ })
+ );
}
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
index a126d06f303b4..718597c12e9c5 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
@@ -7,11 +7,7 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import {
- registerRoleMappingsRoute,
- registerRoleMappingRoute,
- registerNewRoleMappingRoute,
-} from './role_mappings';
+import { registerRoleMappingsRoute, registerRoleMappingRoute } from './role_mappings';
const roleMappingBaseSchema = {
rules: { username: 'user' },
@@ -80,29 +76,6 @@ describe('role mappings routes', () => {
});
});
- describe('GET /api/app_search/role_mappings/{id}', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/app_search/role_mappings/{id}',
- });
-
- registerRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/:id',
- });
- });
- });
-
describe('PUT /api/app_search/role_mappings/{id}', () => {
let mockRouter: MockRouter;
@@ -160,27 +133,4 @@ describe('role mappings routes', () => {
});
});
});
-
- describe('GET /api/app_search/role_mappings/new', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/app_search/role_mappings/new',
- });
-
- registerNewRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/new',
- });
- });
- });
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
index 86e17b575e019..75724a3344d6d 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
@@ -48,20 +48,6 @@ export function registerRoleMappingRoute({
router,
enterpriseSearchRequestHandler,
}: RouteDependencies) {
- router.get(
- {
- path: '/api/app_search/role_mappings/{id}',
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- },
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/:id',
- })
- );
-
router.put(
{
path: '/api/app_search/role_mappings/{id}',
@@ -92,23 +78,7 @@ export function registerRoleMappingRoute({
);
}
-export function registerNewRoleMappingRoute({
- router,
- enterpriseSearchRequestHandler,
-}: RouteDependencies) {
- router.get(
- {
- path: '/api/app_search/role_mappings/new',
- validate: false,
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/new',
- })
- );
-}
-
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
registerRoleMappingsRoute(dependencies);
registerRoleMappingRoute(dependencies);
- registerNewRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
index 0dade134767e4..a945866da5ef2 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
@@ -7,11 +7,7 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import {
- registerOrgRoleMappingsRoute,
- registerOrgRoleMappingRoute,
- registerOrgNewRoleMappingRoute,
-} from './role_mappings';
+import { registerOrgRoleMappingsRoute, registerOrgRoleMappingRoute } from './role_mappings';
describe('role mappings routes', () => {
describe('GET /api/workplace_search/org/role_mappings', () => {
@@ -60,29 +56,6 @@ describe('role mappings routes', () => {
});
});
- describe('GET /api/workplace_search/org/role_mappings/{id}', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/workplace_search/org/role_mappings/{id}',
- });
-
- registerOrgRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/ws/org/role_mappings/:id',
- });
- });
- });
-
describe('PUT /api/workplace_search/org/role_mappings/{id}', () => {
let mockRouter: MockRouter;
@@ -128,27 +101,4 @@ describe('role mappings routes', () => {
});
});
});
-
- describe('GET /api/workplace_search/org/role_mappings/new', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/workplace_search/org/role_mappings/new',
- });
-
- registerOrgNewRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/ws/org/role_mappings/new',
- });
- });
- });
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
index 5a6359c1cd836..a0fcec63cbb27 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
@@ -48,20 +48,6 @@ export function registerOrgRoleMappingRoute({
router,
enterpriseSearchRequestHandler,
}: RouteDependencies) {
- router.get(
- {
- path: '/api/workplace_search/org/role_mappings/{id}',
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- },
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/ws/org/role_mappings/:id',
- })
- );
-
router.put(
{
path: '/api/workplace_search/org/role_mappings/{id}',
@@ -92,23 +78,7 @@ export function registerOrgRoleMappingRoute({
);
}
-export function registerOrgNewRoleMappingRoute({
- router,
- enterpriseSearchRequestHandler,
-}: RouteDependencies) {
- router.get(
- {
- path: '/api/workplace_search/org/role_mappings/new',
- validate: false,
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/ws/org/role_mappings/new',
- })
- );
-}
-
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
registerOrgRoleMappingsRoute(dependencies);
registerOrgRoleMappingRoute(dependencies);
- registerOrgNewRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/fleet/common/constants/agent.ts b/x-pack/plugins/fleet/common/constants/agent.ts
index 6d85f658f2240..e38b7a6b5832b 100644
--- a/x-pack/plugins/fleet/common/constants/agent.ts
+++ b/x-pack/plugins/fleet/common/constants/agent.ts
@@ -25,3 +25,4 @@ export const AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL = 5;
export const AGENTS_INDEX = '.fleet-agents';
export const AGENT_ACTIONS_INDEX = '.fleet-actions';
+export const AGENT_ACTIONS_RESULTS_INDEX = '.fleet-actions-results';
diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts
index e3001542e3e6f..ef8cb63f132b4 100644
--- a/x-pack/plugins/fleet/common/constants/index.ts
+++ b/x-pack/plugins/fleet/common/constants/index.ts
@@ -31,6 +31,7 @@ export const FLEET_SERVER_SERVERS_INDEX = '.fleet-servers';
export const FLEET_SERVER_INDICES = [
'.fleet-actions',
+ '.fleet-actions-results',
'.fleet-agents',
FLEET_SERVER_ARTIFACTS_INDEX,
'.fleet-enrollment-api-keys',
diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts
index 0ef9f8b7ace36..f19684b0445e2 100644
--- a/x-pack/plugins/fleet/common/types/models/epm.ts
+++ b/x-pack/plugins/fleet/common/types/models/epm.ts
@@ -48,7 +48,12 @@ export type EpmPackageInstallStatus = 'installed' | 'installing';
export type DetailViewPanelName = 'overview' | 'policies' | 'settings' | 'custom';
export type ServiceName = 'kibana' | 'elasticsearch';
export type AgentAssetType = typeof agentAssetTypes;
-export type AssetType = KibanaAssetType | ElasticsearchAssetType | ValueOf;
+export type DocAssetType = 'doc' | 'notice';
+export type AssetType =
+ | KibanaAssetType
+ | ElasticsearchAssetType
+ | ValueOf
+ | DocAssetType;
/*
Enum mapping of a saved object asset type to how it would appear in a package file path (snake cased)
@@ -344,6 +349,7 @@ export interface EpmPackageAdditions {
latestVersion: string;
assets: AssetsGroupedByServiceByType;
removable?: boolean;
+ notice?: string;
}
type Merge = Omit> &
diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx
index 49836e9ed4ca6..d707fd162ae02 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx
@@ -18,7 +18,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import type { Section } from '../sections';
-import { AlphaMessaging, SettingFlyout } from '../components';
+import { SettingFlyout } from '../components';
import { useLink, useConfig, useUrlModal } from '../hooks';
interface Props {
@@ -144,7 +144,6 @@ export const DefaultLayout: React.FunctionComponent = ({
) : null}
{children}
-
>
);
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
index 3e4b120a28f8e..75fc06c1a4494 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
@@ -7,7 +7,7 @@
import type { ReactEventHandler } from 'react';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
-import { useRouteMatch, useHistory } from 'react-router-dom';
+import { useRouteMatch, useHistory, useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -49,7 +49,6 @@ import { CreatePackagePolicyPageLayout } from './components';
import type { CreatePackagePolicyFrom, PackagePolicyFormState } from './types';
import type { PackagePolicyValidationResults } from './services';
import { validatePackagePolicy, validationHasErrors } from './services';
-import { StepSelectPackage } from './step_select_package';
import { StepSelectAgentPolicy } from './step_select_agent_policy';
import { StepConfigurePackagePolicy } from './step_configure_package';
import { StepDefinePackagePolicy } from './step_define_package_policy';
@@ -60,9 +59,15 @@ const StepsWithLessPadding = styled(EuiSteps)`
}
`;
+const CustomEuiBottomBar = styled(EuiBottomBar)`
+ // Set a relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar
+ z-index: 50;
+`;
+
interface AddToPolicyParams {
pkgkey: string;
integration?: string;
+ policyId?: string;
}
interface AddFromPolicyParams {
@@ -81,10 +86,14 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
const routeState = useIntraAppState();
const from: CreatePackagePolicyFrom = 'policyId' in params ? 'policy' : 'package';
+ const { search } = useLocation();
+ const queryParams = useMemo(() => new URLSearchParams(search), [search]);
+ const policyId = useMemo(() => queryParams.get('policyId') ?? undefined, [queryParams]);
+
// Agent policy and package info states
- const [agentPolicy, setAgentPolicy] = useState();
+ const [agentPolicy, setAgentPolicy] = useState();
const [packageInfo, setPackageInfo] = useState();
- const [isLoadingSecondStep, setIsLoadingSecondStep] = useState(false);
+ const [isLoadingAgentPolicyStep, setIsLoadingAgentPolicyStep] = useState(false);
// Retrieve agent count
const agentPolicyId = agentPolicy?.id;
@@ -286,6 +295,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
agentPolicyName: agentPolicy.name,
},
})
+ : (params as AddToPolicyParams)?.policyId && agentPolicy && agentCount === 0
+ ? i18n.translate('xpack.fleet.createPackagePolicy.addAgentNextNotification', {
+ defaultMessage: `The policy has been updated. Add an agent to the '{agentPolicyName}' policy to deploy this policy.`,
+ values: {
+ agentPolicyName: agentPolicy.name,
+ },
+ })
: undefined,
'data-test-subj': 'packagePolicyCreateSuccessToast',
});
@@ -337,32 +353,20 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
),
- [params, updatePackageInfo, agentPolicy, updateAgentPolicy]
+ [params, updatePackageInfo, agentPolicy, updateAgentPolicy, policyId]
);
const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create');
- const stepSelectPackage = useMemo(
- () => (
-
- ),
- [params, updateAgentPolicy, packageInfo, updatePackageInfo]
- );
-
const stepConfigurePackagePolicy = useMemo(
() =>
- isLoadingSecondStep ? (
+ isLoadingAgentPolicyStep ? (
) : agentPolicy && packageInfo ? (
<>
@@ -399,7 +403,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
),
[
- isLoadingSecondStep,
+ isLoadingAgentPolicyStep,
agentPolicy,
packageInfo,
packagePolicy,
@@ -413,27 +417,20 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
);
const steps: EuiStepProps[] = [
- from === 'package'
- ? {
- title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle', {
- defaultMessage: 'Select an agent policy',
- }),
- children: stepSelectAgentPolicy,
- }
- : {
- title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectPackageTitle', {
- defaultMessage: 'Select an integration',
- }),
- children: stepSelectPackage,
- },
{
title: i18n.translate('xpack.fleet.createPackagePolicy.stepConfigurePackagePolicyTitle', {
defaultMessage: 'Configure integration',
}),
- status: !packageInfo || !agentPolicy || isLoadingSecondStep ? 'disabled' : undefined,
+ status: !packageInfo || !agentPolicy || isLoadingAgentPolicyStep ? 'disabled' : undefined,
'data-test-subj': 'dataCollectionSetupStep',
children: stepConfigurePackagePolicy,
},
+ {
+ title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle', {
+ defaultMessage: 'Apply to agent policy',
+ }),
+ children: stepSelectAgentPolicy,
+ },
];
return (
@@ -459,10 +456,10 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
)}
-
+
- {!isLoadingSecondStep && agentPolicy && packageInfo && formState === 'INVALID' ? (
+ {!isLoadingAgentPolicyStep && agentPolicy && packageInfo && formState === 'INVALID' ? (
{
-
+
);
};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx
index 26d47cbff5b86..e2f5a7249ff0a 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx
@@ -14,9 +14,11 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiComboBox,
- EuiTextColor,
EuiPortal,
EuiFormRow,
+ EuiDescribedFormGroup,
+ EuiTitle,
+ EuiText,
EuiLink,
} from '@elastic/eui';
@@ -32,38 +34,32 @@ import {
} from '../../../hooks';
import { CreateAgentPolicyFlyout } from '../list_page/components';
-const AgentPolicyWrapper = styled(EuiFormRow)`
+const AgentPolicyFormRow = styled(EuiFormRow)`
.euiFormRow__label {
width: 100%;
}
`;
-// Custom styling for drop down list items due to:
-// 1) the max-width and overflow properties is added to prevent long agent policy
-// names/descriptions from overflowing the flex items
-// 2) max-width is built from the grow property on the flex items because the value
-// changes based on if Fleet is enabled/setup or not
-const AgentPolicyNameColumn = styled(EuiFlexItem)`
- max-width: ${(props) => `${((props.grow as number) / 9) * 100}%`};
- overflow: hidden;
-`;
-const AgentPolicyDescriptionColumn = styled(EuiFlexItem)`
- max-width: ${(props) => `${((props.grow as number) / 9) * 100}%`};
- overflow: hidden;
-`;
-
export const StepSelectAgentPolicy: React.FunctionComponent<{
pkgkey: string;
updatePackageInfo: (packageInfo: PackageInfo | undefined) => void;
+ defaultAgentPolicyId?: string;
agentPolicy: AgentPolicy | undefined;
updateAgentPolicy: (agentPolicy: AgentPolicy | undefined) => void;
setIsLoadingSecondStep: (isLoading: boolean) => void;
-}> = ({ pkgkey, updatePackageInfo, agentPolicy, updateAgentPolicy, setIsLoadingSecondStep }) => {
+}> = ({
+ pkgkey,
+ updatePackageInfo,
+ agentPolicy,
+ updateAgentPolicy,
+ setIsLoadingSecondStep,
+ defaultAgentPolicyId,
+}) => {
const { isReady: isFleetReady } = useFleetStatus();
// Selected agent policy state
const [selectedPolicyId, setSelectedPolicyId] = useState(
- agentPolicy ? agentPolicy.id : undefined
+ agentPolicy?.id ?? defaultAgentPolicyId
);
const [selectedAgentPolicyError, setSelectedAgentPolicyError] = useState();
@@ -223,95 +219,91 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
) : null}
-
-
+
+
-
-
-
- setIsCreateAgentPolicyFlyoutOpen(true)}
- >
-
-
-
-
-
+
+
}
- helpText={
- isFleetReady && selectedPolicyId ? (
-
- ) : null
+ description={
+
+
+
+
+
}
>
- ) => {
- return (
-
-
- {option.label}
-
-
-
- {agentPoliciesById[option.value!].description}
-
-
- {isFleetReady ? (
-
-
-
-
-
- ) : null}
-
- );
- }}
- selectedOptions={selectedAgentPolicyOption ? [selectedAgentPolicyOption] : []}
- onChange={(options) => {
- const selectedOption = options[0] || undefined;
- if (selectedOption) {
- if (selectedOption.value !== selectedPolicyId) {
- setSelectedPolicyId(selectedOption.value);
+ label={
+
+
+
+
+
+
+ setIsCreateAgentPolicyFlyoutOpen(true)}
+ >
+
+
+
+
+
+ }
+ helpText={
+ isFleetReady && selectedPolicyId ? (
+
+ ) : null
+ }
+ >
+
-
+ )}
+ singleSelection={{ asPlainText: true }}
+ isClearable={false}
+ fullWidth={true}
+ isLoading={isAgentPoliciesLoading || isPackageInfoLoading}
+ options={agentPolicyOptions}
+ selectedOptions={selectedAgentPolicyOption ? [selectedAgentPolicyOption] : []}
+ onChange={(options) => {
+ const selectedOption = options[0] || undefined;
+ if (selectedOption) {
+ if (selectedOption.value !== selectedPolicyId) {
+ setSelectedPolicyId(selectedOption.value);
+ }
+ } else {
+ setSelectedPolicyId(undefined);
+ }
+ }}
+ />
+
+
{/* Display selected agent policy error if there is one */}
{selectedAgentPolicyError ? (
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx
deleted file mode 100644
index 50c63274b5e85..0000000000000
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useEffect, useState, Fragment } from 'react';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer } from '@elastic/eui';
-
-import { Error, PackageIcon } from '../../../components';
-import type { AgentPolicy, PackageInfo, PackagePolicy, GetPackagesResponse } from '../../../types';
-import {
- useGetOneAgentPolicy,
- useGetPackages,
- useGetLimitedPackages,
- sendGetPackageInfoByKey,
-} from '../../../hooks';
-import { pkgKeyFromPackageInfo } from '../../../services';
-
-export const StepSelectPackage: React.FunctionComponent<{
- agentPolicyId: string;
- updateAgentPolicy: (agentPolicy: AgentPolicy | undefined) => void;
- packageInfo?: PackageInfo;
- updatePackageInfo: (packageInfo: PackageInfo | undefined) => void;
- setIsLoadingSecondStep: (isLoading: boolean) => void;
-}> = ({
- agentPolicyId,
- updateAgentPolicy,
- packageInfo,
- updatePackageInfo,
- setIsLoadingSecondStep,
-}) => {
- // Selected package state
- const [selectedPkgKey, setSelectedPkgKey] = useState(
- packageInfo ? pkgKeyFromPackageInfo(packageInfo) : undefined
- );
- const [selectedPkgError, setSelectedPkgError] = useState();
-
- // Fetch agent policy info
- const {
- data: agentPolicyData,
- error: agentPolicyError,
- isLoading: isAgentPoliciesLoading,
- } = useGetOneAgentPolicy(agentPolicyId);
-
- // Fetch packages info
- // Filter out limited packages already part of selected agent policy
- const [packages, setPackages] = useState([]);
- const {
- data: packagesData,
- error: packagesError,
- isLoading: isPackagesLoading,
- } = useGetPackages();
- const {
- data: limitedPackagesData,
- isLoading: isLimitedPackagesLoading,
- } = useGetLimitedPackages();
- useEffect(() => {
- if (packagesData?.response && limitedPackagesData?.response && agentPolicyData?.item) {
- const allPackages = packagesData.response;
- const limitedPackages = limitedPackagesData.response;
- const usedLimitedPackages = (agentPolicyData.item.package_policies as PackagePolicy[])
- .map((packagePolicy) => packagePolicy.package?.name || '')
- .filter((pkgName) => limitedPackages.includes(pkgName));
- setPackages(allPackages.filter((pkg) => !usedLimitedPackages.includes(pkg.name)));
- }
- }, [packagesData, limitedPackagesData, agentPolicyData]);
-
- // Update parent agent policy state
- useEffect(() => {
- if (agentPolicyData && agentPolicyData.item) {
- updateAgentPolicy(agentPolicyData.item);
- }
- }, [agentPolicyData, updateAgentPolicy]);
-
- // Update parent selected package state
- useEffect(() => {
- const fetchPackageInfo = async () => {
- if (selectedPkgKey) {
- setIsLoadingSecondStep(true);
- const { data, error } = await sendGetPackageInfoByKey(selectedPkgKey);
- if (error) {
- setSelectedPkgError(error);
- updatePackageInfo(undefined);
- } else if (data && data.response) {
- setSelectedPkgError(undefined);
- updatePackageInfo(data.response);
- }
- setIsLoadingSecondStep(false);
- } else {
- setSelectedPkgError(undefined);
- updatePackageInfo(undefined);
- }
- };
- if (!packageInfo || selectedPkgKey !== pkgKeyFromPackageInfo(packageInfo)) {
- fetchPackageInfo();
- }
- }, [selectedPkgKey, packageInfo, updatePackageInfo, setIsLoadingSecondStep]);
-
- // Display agent policy error if there is one
- if (agentPolicyError) {
- return (
-
- }
- error={agentPolicyError}
- />
- );
- }
-
- // Display packages list error if there is one
- if (packagesError) {
- return (
-
- }
- error={packagesError}
- />
- );
- }
-
- return (
-
-
- {
- const pkgkey = `${name}-${version}`;
- return {
- label: title || name,
- key: pkgkey,
- prepend: ,
- checked: selectedPkgKey === pkgkey ? 'on' : undefined,
- };
- })}
- listProps={{
- bordered: true,
- }}
- searchProps={{
- placeholder: i18n.translate(
- 'xpack.fleet.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder',
- {
- defaultMessage: 'Search for integrations',
- }
- ),
- }}
- height={240}
- onChange={(options) => {
- const selectedOption = options.find((option) => option.checked === 'on');
- if (selectedOption) {
- if (selectedOption.key !== selectedPkgKey) {
- setSelectedPkgKey(selectedOption.key);
- }
- } else {
- setSelectedPkgKey(undefined);
- }
- }}
- >
- {(list, search) => (
-
- {search}
-
- {list}
-
- )}
-
-
- {/* Display selected package error if there is one */}
- {selectedPkgError ? (
-
-
- }
- error={selectedPkgError}
- />
-
- ) : null}
-
- );
-};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
index 2e6e7fb984ef0..49af14b7234fa 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
@@ -18,9 +18,11 @@ import {
EuiText,
} from '@elastic/eui';
+import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../common';
+import { pagePathGetters } from '../../../../../../../constants';
import type { AgentPolicy, PackagePolicy } from '../../../../../types';
import { PackageIcon, PackagePolicyActionsMenu } from '../../../../../components';
-import { useCapabilities, useLink } from '../../../../../hooks';
+import { useCapabilities, useStartServices } from '../../../../../hooks';
interface InMemoryPackagePolicy extends PackagePolicy {
packageName?: string;
@@ -49,7 +51,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({
agentPolicy,
...rest
}) => {
- const { getHref } = useLink();
+ const { application } = useStartServices();
const hasWriteCapabilities = useCapabilities().write;
// With the package policies provided on input, generate the list of package policies
@@ -194,8 +196,13 @@ export const PackagePoliciesTable: React.FunctionComponent = ({
{
+ application.navigateToApp(INTEGRATIONS_PLUGIN_ID, {
+ path: `#${pagePathGetters.integrations_all()[1]}`,
+ state: { forAgentPolicyId: agentPolicy.id },
+ });
+ }}
>
-
- {children}
-
+
+
+ {children}
+
+
diff --git a/x-pack/plugins/fleet/public/applications/integrations/constants.tsx b/x-pack/plugins/fleet/public/applications/integrations/constants.tsx
index 403a47f4b94b2..08197e18fec02 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/constants.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/constants.tsx
@@ -7,7 +7,7 @@
import type { IconType } from '@elastic/eui';
-import type { AssetType, ServiceName } from '../../types';
+import type { ServiceName } from '../../types';
import { ElasticsearchAssetType, KibanaAssetType } from '../../types';
export * from '../../constants';
@@ -20,8 +20,9 @@ export const DisplayedAssets: ServiceNameToAssetTypes = {
kibana: Object.values(KibanaAssetType),
elasticsearch: Object.values(ElasticsearchAssetType),
};
+export type DisplayedAssetType = KibanaAssetType | ElasticsearchAssetType;
-export const AssetTitleMap: Record = {
+export const AssetTitleMap: Record = {
dashboard: 'Dashboard',
ilm_policy: 'ILM Policy',
ingest_pipeline: 'Ingest Pipeline',
@@ -31,7 +32,6 @@ export const AssetTitleMap: Record = {
component_template: 'Component Template',
search: 'Saved Search',
visualization: 'Visualization',
- input: 'Agent input',
map: 'Map',
data_stream_ilm_policy: 'Data Stream ILM Policy',
lens: 'Lens',
diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts b/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts
index 7f758e3c472c1..9e4cdde064e70 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts
+++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts
@@ -10,3 +10,4 @@ export { useBreadcrumbs } from './use_breadcrumbs';
export { useLinks } from './use_links';
export * from './use_local_search';
export * from './use_package_install';
+export * from './use_agent_policy_context';
diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_agent_policy_context.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_agent_policy_context.tsx
new file mode 100644
index 0000000000000..859db79ad159b
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_agent_policy_context.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { FunctionComponent } from 'react';
+import React, { createContext, useContext, useRef, useCallback } from 'react';
+
+import type { IntegrationsAppBrowseRouteState } from '../../../types';
+import { useIntraAppState } from '../../../hooks';
+
+interface AgentPolicyContextValue {
+ getId(): string | undefined;
+}
+
+const AgentPolicyContext = createContext({ getId: () => undefined });
+
+export const AgentPolicyContextProvider: FunctionComponent = ({ children }) => {
+ const maybeState = useIntraAppState();
+ const ref = useRef(maybeState?.forAgentPolicyId);
+
+ const getId = useCallback(() => {
+ return ref.current;
+ }, []);
+ return {children} ;
+};
+
+export const useAgentPolicyContext = () => {
+ const ctx = useContext(AgentPolicyContext);
+ if (!ctx) {
+ throw new Error('useAgentPolicyContext can only be used inside of AgentPolicyContextProvider');
+ }
+ return ctx;
+};
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx
index 6ddff968bd3f3..41db09b0538b9 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx
@@ -7,7 +7,7 @@
import type { IconType } from '@elastic/eui';
-import type { AssetType, ServiceName } from '../../types';
+import type { ServiceName } from '../../types';
import { ElasticsearchAssetType, KibanaAssetType } from '../../types';
// only allow Kibana assets for the kibana key, ES asssets for elasticsearch, etc
@@ -19,7 +19,9 @@ export const DisplayedAssets: ServiceNameToAssetTypes = {
elasticsearch: Object.values(ElasticsearchAssetType),
};
-export const AssetTitleMap: Record = {
+export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType;
+
+export const AssetTitleMap: Record = {
dashboard: 'Dashboard',
ilm_policy: 'ILM Policy',
ingest_pipeline: 'Ingest Pipeline',
@@ -29,7 +31,6 @@ export const AssetTitleMap: Record = {
component_template: 'Component Template',
search: 'Saved Search',
visualization: 'Visualization',
- input: 'Agent input',
map: 'Map',
data_stream_ilm_policy: 'Data Stream ILM Policy',
lens: 'Lens',
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
index 68736455b818f..99a29a8194f9b 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
@@ -37,7 +37,12 @@ import {
INTEGRATIONS_ROUTING_PATHS,
pagePathGetters,
} from '../../../../constants';
-import { useCapabilities, useGetPackageInfoByKey, useLink } from '../../../../hooks';
+import {
+ useCapabilities,
+ useGetPackageInfoByKey,
+ useLink,
+ useAgentPolicyContext,
+} from '../../../../hooks';
import { pkgKeyFromPackageInfo } from '../../../../services';
import type {
CreatePackagePolicyRouteState,
@@ -79,6 +84,7 @@ function Breadcrumbs({ packageTitle }: { packageTitle: string }) {
}
export function Detail() {
+ const { getId: getAgentPolicyId } = useAgentPolicyContext();
const { pkgkey, panel } = useParams();
const { getHref } = useLink();
const hasWriteCapabilites = useCapabilities().write;
@@ -87,6 +93,7 @@ export function Detail() {
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
const integration = useMemo(() => queryParams.get('integration'), [queryParams]);
const services = useStartServices();
+ const agentPolicyIdFromContext = getAgentPolicyId();
// Package info state
const [packageInfo, setPackageInfo] = useState(null);
@@ -211,24 +218,42 @@ export function Detail() {
search,
hash,
});
- const redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] &
- CreatePackagePolicyRouteState['onCancelNavigateTo'] = [
- INTEGRATIONS_PLUGIN_ID,
- {
- path: currentPath,
- },
- ];
- const redirectBackRouteState: CreatePackagePolicyRouteState = {
- onSaveNavigateTo: redirectToPath,
- onCancelNavigateTo: redirectToPath,
- onCancelUrl: currentPath,
- };
const path = pagePathGetters.add_integration_to_policy({
pkgkey,
...(integration ? { integration } : {}),
+ ...(agentPolicyIdFromContext ? { agentPolicyId: agentPolicyIdFromContext } : {}),
})[1];
+ let redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] &
+ CreatePackagePolicyRouteState['onCancelNavigateTo'];
+
+ if (agentPolicyIdFromContext) {
+ redirectToPath = [
+ PLUGIN_ID,
+ {
+ path: `#${
+ pagePathGetters.policy_details({
+ policyId: agentPolicyIdFromContext,
+ })[1]
+ }`,
+ },
+ ];
+ } else {
+ redirectToPath = [
+ INTEGRATIONS_PLUGIN_ID,
+ {
+ path: currentPath,
+ },
+ ];
+ }
+
+ const redirectBackRouteState: CreatePackagePolicyRouteState = {
+ onSaveNavigateTo: redirectToPath,
+ onCancelNavigateTo: redirectToPath,
+ onCancelUrl: currentPath,
+ };
+
services.application.navigateToApp(PLUGIN_ID, {
// Necessary because of Fleet's HashRouter. Can be changed when
// https://github.com/elastic/kibana/issues/96134 is resolved
@@ -236,7 +261,16 @@ export function Detail() {
state: redirectBackRouteState,
});
},
- [history, hash, pathname, search, pkgkey, integration, services.application]
+ [
+ history,
+ hash,
+ pathname,
+ search,
+ pkgkey,
+ integration,
+ services.application,
+ agentPolicyIdFromContext,
+ ]
);
const headerRightContent = useMemo(
@@ -284,6 +318,9 @@ export function Detail() {
href={getHref('add_integration_to_policy', {
pkgkey,
...(integration ? { integration } : {}),
+ ...(agentPolicyIdFromContext
+ ? { agentPolicyId: agentPolicyIdFromContext }
+ : {}),
})}
onClick={handleAddIntegrationPolicyClick}
data-test-subj="addIntegrationPolicyButton"
@@ -325,6 +362,7 @@ export function Detail() {
packageInstallStatus,
pkgkey,
updateAvailable,
+ agentPolicyIdFromContext,
]
);
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx
index 487df17980345..0a601d2128bba 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import React, { memo, useMemo } from 'react';
+import React, { memo, useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
@@ -13,6 +13,8 @@ import {
EuiTextColor,
EuiDescriptionList,
EuiNotificationBadge,
+ EuiLink,
+ EuiPortal,
} from '@elastic/eui';
import type { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
@@ -26,6 +28,8 @@ import { entries } from '../../../../../types';
import { useGetCategories } from '../../../../../hooks';
import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants';
+import { NoticeModal } from './notice_modal';
+
interface Props {
packageInfo: PackageInfo;
}
@@ -41,6 +45,11 @@ export const Details: React.FC = memo(({ packageInfo }) => {
return [];
}, [categoriesData, isLoadingCategories, packageInfo.categories]);
+ const [isNoticeModalOpen, setIsNoticeModalOpen] = useState(false);
+ const toggleNoticeModal = useCallback(() => {
+ setIsNoticeModalOpen(!isNoticeModalOpen);
+ }, [isNoticeModalOpen]);
+
const listItems = useMemo(() => {
// Base details: version and categories
const items: EuiDescriptionListProps['listItems'] = [
@@ -123,14 +132,23 @@ export const Details: React.FC = memo(({ packageInfo }) => {
}
// License details
- if (packageInfo.license) {
+ if (packageInfo.license || packageInfo.notice) {
items.push({
title: (
),
- description: packageInfo.license,
+ description: (
+ <>
+ {packageInfo.license}
+ {packageInfo.notice && (
+
+ NOTICE.txt
+
+ )}
+ >
+ ),
});
}
@@ -140,21 +158,30 @@ export const Details: React.FC = memo(({ packageInfo }) => {
packageInfo.assets,
packageInfo.data_streams,
packageInfo.license,
+ packageInfo.notice,
packageInfo.version,
+ toggleNoticeModal,
]);
return (
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+ {isNoticeModalOpen && packageInfo.notice && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
});
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/notice_modal.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/notice_modal.tsx
new file mode 100644
index 0000000000000..239bd133d3c91
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/notice_modal.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+import {
+ EuiCodeBlock,
+ EuiLoadingContent,
+ EuiModal,
+ EuiModalBody,
+ EuiModalHeader,
+ EuiModalFooter,
+ EuiModalHeaderTitle,
+ EuiButton,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { sendGetFileByPath, useStartServices } from '../../../../../hooks';
+
+interface Props {
+ noticePath: string;
+ onClose: () => void;
+}
+
+export const NoticeModal: React.FunctionComponent = ({ noticePath, onClose }) => {
+ const { notifications } = useStartServices();
+ const [notice, setNotice] = useState(undefined);
+
+ useEffect(() => {
+ async function fetchData() {
+ try {
+ const { data } = await sendGetFileByPath(noticePath);
+ setNotice(data || '');
+ } catch (err) {
+ notifications.toasts.addError(err, {
+ title: i18n.translate('xpack.fleet.epm.errorLoadingNotice', {
+ defaultMessage: 'Error loading NOTICE.txt',
+ }),
+ });
+ }
+ }
+ fetchData();
+ }, [noticePath, notifications]);
+ return (
+
+
+
+ NOTICE.txt
+
+
+
+
+ {notice ? (
+ notice
+ ) : (
+ // Simulate a long notice while loading
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts
index 4ff71cf162e22..3c9c0e5759615 100644
--- a/x-pack/plugins/fleet/public/constants/page_paths.ts
+++ b/x-pack/plugins/fleet/public/constants/page_paths.ts
@@ -111,9 +111,10 @@ export const pagePathGetters: {
FLEET_BASE_PATH,
`/policies/${policyId}/add-integration`,
],
- add_integration_to_policy: ({ pkgkey, integration }) => [
+ add_integration_to_policy: ({ pkgkey, integration, agentPolicyId }) => [
FLEET_BASE_PATH,
- `/integrations/${pkgkey}/add-integration${integration ? `/${integration}` : ''}`,
+ // prettier-ignore
+ `/integrations/${pkgkey}/add-integration${integration ? `/${integration}` : ''}${agentPolicyId ? `?policyId=${agentPolicyId}` : ''}`,
],
edit_integration: ({ policyId, packagePolicyId }) => [
FLEET_BASE_PATH,
diff --git a/x-pack/plugins/fleet/public/types/intra_app_route_state.ts b/x-pack/plugins/fleet/public/types/intra_app_route_state.ts
index 74921daa0d2a1..36fd32c2a6584 100644
--- a/x-pack/plugins/fleet/public/types/intra_app_route_state.ts
+++ b/x-pack/plugins/fleet/public/types/intra_app_route_state.ts
@@ -39,6 +39,11 @@ export interface AgentDetailsReassignPolicyAction {
onDoneNavigateTo?: Parameters;
}
+export interface IntegrationsAppBrowseRouteState {
+ /** The agent policy that we are browsing integrations for */
+ forAgentPolicyId: string;
+}
+
/**
* All possible Route states.
*/
diff --git a/x-pack/plugins/fleet/server/services/epm/archive/index.ts b/x-pack/plugins/fleet/server/services/epm/archive/index.ts
index 809684df0592c..b08ec815a394d 100644
--- a/x-pack/plugins/fleet/server/services/epm/archive/index.ts
+++ b/x-pack/plugins/fleet/server/services/epm/archive/index.ts
@@ -114,6 +114,13 @@ export function getPathParts(path: string): AssetParts {
[pkgkey, service, type, file] = path.replace(`data_stream/${dataset}/`, '').split('/');
}
+ // To support the NOTICE asset at the root level
+ if (service === 'NOTICE.txt') {
+ file = service;
+ type = 'notice';
+ service = '';
+ }
+
// This is to cover for the fields.yml files inside the "fields" directory
if (file === undefined) {
file = type;
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts
index e4e4f9c55fd2b..404431816d10c 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts
@@ -126,6 +126,7 @@ export async function getPackageInfo(options: {
title: packageInfo.title || nameAsTitle(packageInfo.name),
assets: Registry.groupPathsByService(paths || []),
removable: !isRequiredPackage(pkgName),
+ notice: Registry.getNoticePath(paths || []),
};
const updated = { ...packageInfo, ...additions };
diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts
index 5ee7e198555c5..011a0e74e8c18 100644
--- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts
+++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts
@@ -255,3 +255,15 @@ export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByTy
elasticsearch: assets.elasticsearch,
};
}
+
+export function getNoticePath(paths: string[]): string | undefined {
+ for (const path of paths) {
+ const parts = getPathParts(path.replace(/^\/package\//, ''));
+ if (parts.type === 'notice') {
+ const { pkgName, pkgVersion } = splitPkgKey(parts.pkgkey);
+ return `/package/${pkgName}/${pkgVersion}/${parts.file}`;
+ }
+ }
+
+ return undefined;
+}
diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts
index a6958ba88449a..b3626a83c41d1 100644
--- a/x-pack/plugins/fleet/server/services/package_policy.test.ts
+++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts
@@ -688,6 +688,129 @@ describe('Package policy service', () => {
expect(modifiedStream.vars!.paths.value).toEqual(expect.arrayContaining(['north', 'south']));
expect(modifiedStream.vars!.period.value).toEqual('12mo');
});
+
+ it('should add new input vars when updating', async () => {
+ const savedObjectsClient = savedObjectsClientMock.create();
+ const mockPackagePolicy = createPackagePolicyMock();
+ const mockInputs = [
+ {
+ config: {},
+ enabled: true,
+ keep_enabled: true,
+ type: 'endpoint',
+ vars: {
+ dog: {
+ type: 'text',
+ value: 'dalmatian',
+ },
+ cat: {
+ type: 'text',
+ value: 'siamese',
+ frozen: true,
+ },
+ },
+ streams: [
+ {
+ data_stream: {
+ type: 'birds',
+ dataset: 'migratory.patterns',
+ },
+ enabled: false,
+ id: `endpoint-migratory.patterns-${mockPackagePolicy.id}`,
+ vars: {
+ paths: {
+ value: ['north', 'south'],
+ type: 'text',
+ frozen: true,
+ },
+ },
+ },
+ ],
+ },
+ ];
+ const inputsUpdate = [
+ {
+ config: {},
+ enabled: false,
+ type: 'endpoint',
+ vars: {
+ dog: {
+ type: 'text',
+ value: 'labrador',
+ },
+ cat: {
+ type: 'text',
+ value: 'tabby',
+ },
+ },
+ streams: [
+ {
+ data_stream: {
+ type: 'birds',
+ dataset: 'migratory.patterns',
+ },
+ enabled: false,
+ id: `endpoint-migratory.patterns-${mockPackagePolicy.id}`,
+ vars: {
+ paths: {
+ value: ['east', 'west'],
+ type: 'text',
+ },
+ period: {
+ value: '12mo',
+ type: 'text',
+ },
+ },
+ },
+ ],
+ },
+ ];
+ const attributes = {
+ ...mockPackagePolicy,
+ inputs: mockInputs,
+ };
+
+ savedObjectsClient.get.mockResolvedValue({
+ id: 'test',
+ type: 'abcd',
+ references: [],
+ version: 'test',
+ attributes,
+ });
+
+ savedObjectsClient.update.mockImplementation(
+ async (
+ type: string,
+ id: string,
+ attrs: any
+ ): Promise> => {
+ savedObjectsClient.get.mockResolvedValue({
+ id: 'test',
+ type: 'abcd',
+ references: [],
+ version: 'test',
+ attributes: attrs,
+ });
+ return attrs;
+ }
+ );
+ const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+
+ const result = await packagePolicyService.update(
+ savedObjectsClient,
+ elasticsearchClient,
+ 'the-package-policy-id',
+ { ...mockPackagePolicy, inputs: inputsUpdate }
+ );
+
+ const [modifiedInput] = result.inputs;
+ expect(modifiedInput.enabled).toEqual(true);
+ expect(modifiedInput.vars!.dog.value).toEqual('labrador');
+ expect(modifiedInput.vars!.cat.value).toEqual('siamese');
+ const [modifiedStream] = modifiedInput.streams;
+ expect(modifiedStream.vars!.paths.value).toEqual(expect.arrayContaining(['north', 'south']));
+ expect(modifiedStream.vars!.period.value).toEqual('12mo');
+ });
});
describe('runExternalCallbacks', () => {
diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts
index 93bcef458279c..1cda159429984 100644
--- a/x-pack/plugins/fleet/server/services/package_policy.ts
+++ b/x-pack/plugins/fleet/server/services/package_policy.ts
@@ -649,11 +649,16 @@ function _enforceFrozenVars(
newVars: Record
) {
const resultVars: Record = {};
+ for (const [key, val] of Object.entries(newVars)) {
+ if (oldVars[key]?.frozen) {
+ resultVars[key] = oldVars[key];
+ } else {
+ resultVars[key] = val;
+ }
+ }
for (const [key, val] of Object.entries(oldVars)) {
- if (val.frozen) {
+ if (!newVars[key] && val.frozen) {
resultVars[key] = val;
- } else {
- resultVars[key] = newVars[key];
}
}
return resultVars;
diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts
index 0b80e18f3fdb2..26e86bbc3d886 100644
--- a/x-pack/plugins/graph/public/application.ts
+++ b/x-pack/plugins/graph/public/application.ts
@@ -31,7 +31,7 @@ import {
} from 'kibana/public';
// @ts-ignore
import { initGraphApp } from './app';
-import { Plugin as DataPlugin, IndexPatternsContract } from '../../../../src/plugins/data/public';
+import { DataPlugin, IndexPatternsContract } from '../../../../src/plugins/data/public';
import { LicensingPluginStart } from '../../licensing/public';
import { checkLicense } from '../common/check_license';
import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public';
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
index 8d839e196916b..556ac35d0565e 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
@@ -46,71 +46,67 @@ Array [
`;
exports[`policy table should show empty state when there are not any policies 1`] = `
-
+
+
-
-
-
-
- Create your first index lifecycle policy
-
-
-
-
- An index lifecycle policy helps you manage your indices as they age.
-
-
-
+ Create your first index lifecycle policy
+
-
+
+ An index lifecycle policy helps you manage your indices as they age.
+
+
+
+
+
+
+
+
-
-
- Create policy
-
+ Create policy
-
-
+
+
-
+
`;
exports[`policy table should sort when linked indices header is clicked 1`] = `
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
index fe82b20093072..07c2228863b81 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
@@ -7,7 +7,7 @@
import React, { useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
-import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
+import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { MIN_SEARCHABLE_SNAPSHOT_LICENSE } from '../../../../common/constants';
@@ -52,43 +52,47 @@ export const EditPolicy: React.FunctionComponent }
- body={
-
- }
- />
+
+ }
+ body={
+
+ }
+ />
+
);
}
if (error || !policies) {
const { statusCode, message } = error ? error : { statusCode: '', message: '' };
return (
-
-
-
- }
- body={
-
- {message} ({statusCode})
-
- }
- actions={
-
-
-
- }
- />
+
+
+
+
+ }
+ body={
+
+ {message} ({statusCode})
+
+ }
+ actions={
+
+
+
+ }
+ />
+
);
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
index d7368249d76e8..172e8259b87af 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
@@ -19,15 +19,10 @@ import {
EuiFlexItem,
EuiFormRow,
EuiHorizontalRule,
- EuiPage,
- EuiPageBody,
- EuiPageContent,
- EuiPageContentHeader,
- EuiPageContentHeaderSection,
EuiSpacer,
EuiSwitch,
EuiText,
- EuiTitle,
+ EuiPageHeader,
} from '@elastic/eui';
import { TextField, useForm, useFormData, useKibana } from '../../../shared_imports';
@@ -153,201 +148,199 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => {
};
return (
-
-
-
-
-
-
-
- {isNewPolicy
- ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', {
- defaultMessage: 'Create policy',
- })
- : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', {
- defaultMessage: 'Edit policy {originalPolicyName}',
- values: { originalPolicyName },
- })}
-
-
-
-
-
+ <>
+
+ {isNewPolicy
+ ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', {
+ defaultMessage: 'Create policy',
+ })
+ : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', {
+ defaultMessage: 'Edit policy {originalPolicyName}',
+ values: { originalPolicyName },
+ })}
+
+ }
+ bottomBorder
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
+
-
-
-
-
-
-
+
+
+ {isShowingPolicyJsonFlyout ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {isShowingPolicyJsonFlyout ? (
+ setIsShowingPolicyJsonFlyout(false)}
+ />
+ ) : null}
+
+ >
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx
index 7cf50de0ee999..deac2bb239d30 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx
@@ -8,7 +8,7 @@
import React, { useEffect } from 'react';
import { ApplicationStart } from 'kibana/public';
import { RouteComponentProps } from 'react-router-dom';
-import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
+import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { PolicyTable as PresentationComponent } from './policy_table';
import { useKibana } from '../../../shared_imports';
@@ -33,43 +33,47 @@ export const PolicyTable: React.FunctionComponent =
if (isLoading) {
return (
- }
- body={
-
- }
- />
+
+ }
+ body={
+
+ }
+ />
+
);
}
if (error) {
const { statusCode, message } = error ? error : { statusCode: '', message: '' };
return (
-
-
-
- }
- body={
-
- {message} ({statusCode})
-
- }
- actions={
-
-
-
- }
- />
+
+
+
+
+ }
+ body={
+
+ {message} ({statusCode})
+
+ }
+ actions={
+
+
+
+ }
+ />
+
);
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx
index 3b0f815800ba3..ba89d6c895d93 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx
@@ -16,9 +16,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
- EuiTitle,
- EuiText,
- EuiPageBody,
+ EuiPageHeader,
EuiPageContent,
} from '@elastic/eui';
import { ApplicationStart } from 'kibana/public';
@@ -119,67 +117,60 @@ export const PolicyTable: React.FunctionComponent = ({
);
} else {
return (
-
-
-
+
+
+
+
+ }
+ body={
+
+
-
- }
- body={
-
-
-
-
-
- }
- actions={createPolicyButton}
- />
-
-
+
+
+ }
+ actions={createPolicyButton}
+ />
+
);
}
return (
-
-
- {confirmModal}
+ <>
+ {confirmModal}
-
-
-
-
-
-
-
-
- {createPolicyButton}
-
-
-
-
+
-
-
+
+ }
+ description={
+
+ }
+ bottomBorder
+ rightSideItems={[createPolicyButton]}
+ />
-
- {content}
-
-
+
+
+ {content}
+ >
);
};
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
index e43f147a65800..bf1a78e3cfe90 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
@@ -31,7 +31,7 @@ describe('Index Templates tab', () => {
server.restore();
});
- describe('when there are no index templates', () => {
+ describe('when there are no index templates of either kind', () => {
test('should display an empty prompt', async () => {
httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] });
@@ -46,6 +46,26 @@ describe('Index Templates tab', () => {
});
});
+ describe('when there are composable index templates but no legacy index templates', () => {
+ test('only the composable index templates table is visible', async () => {
+ httpRequestsMockHelpers.setLoadTemplatesResponse({
+ templates: [fixtures.getComposableTemplate()],
+ legacyTemplates: [],
+ });
+
+ await act(async () => {
+ testBed = await setup();
+ });
+ const { exists, component } = testBed;
+ component.update();
+
+ expect(exists('sectionLoading')).toBe(false);
+ expect(exists('emptyPrompt')).toBe(false);
+ expect(exists('templateTable')).toBe(true);
+ expect(exists('legacyTemplateTable')).toBe(false);
+ });
+ });
+
describe('when there are index templates', () => {
// Add a default loadIndexTemplate response
httpRequestsMockHelpers.setLoadTemplateResponse(fixtures.getTemplate());
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts
index 199ace6048bde..7d3b34a6b8238 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts
@@ -11,17 +11,23 @@ import { WithAppDependencies } from '../helpers';
import { formSetup, TestSubjects } from './template_form.helpers';
-const testBedConfig: TestBedConfig = {
- memoryRouter: {
- initialEntries: [`/create_template`],
- componentRoutePath: `/create_template`,
- },
- doMountAsync: true,
-};
+export const setup: any = (isLegacy: boolean = false) => {
+ const route = isLegacy
+ ? { pathname: '/create_template', search: '?legacy=true' }
+ : { pathname: '/create_template' };
+
+ const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: [route],
+ componentRoutePath: route,
+ },
+ doMountAsync: true,
+ };
-const initTestBed = registerTestBed(
- WithAppDependencies(TemplateCreate),
- testBedConfig
-);
+ const initTestBed = registerTestBed(
+ WithAppDependencies(TemplateCreate),
+ testBedConfig
+ );
-export const setup: any = formSetup.bind(null, initTestBed);
+ return formSetup.call(null, initTestBed);
+};
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx
index 9f435fac8b347..77ce172f3e0db 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx
@@ -101,7 +101,7 @@ describe(' ', () => {
(window as any)['__react-beautiful-dnd-disable-dev-warnings'] = false;
});
- describe('on component mount', () => {
+ describe('composable index template', () => {
beforeEach(async () => {
await act(async () => {
testBed = await setup();
@@ -115,6 +115,11 @@ describe(' ', () => {
expect(find('pageTitle').text()).toEqual('Create template');
});
+ test('renders no deprecation warning', async () => {
+ const { exists } = testBed;
+ expect(exists('legacyIndexTemplateDeprecationWarning')).toBe(false);
+ });
+
test('should not let the user go to the next step with invalid fields', async () => {
const { find, actions, component } = testBed;
@@ -129,6 +134,26 @@ describe(' ', () => {
});
});
+ describe('legacy index template', () => {
+ beforeEach(async () => {
+ await act(async () => {
+ testBed = await setup(true);
+ });
+ });
+
+ test('should set the correct page title', () => {
+ const { exists, find } = testBed;
+
+ expect(exists('pageTitle')).toBe(true);
+ expect(find('pageTitle').text()).toEqual('Create legacy template');
+ });
+
+ test('renders deprecation warning', async () => {
+ const { exists } = testBed;
+ expect(exists('legacyIndexTemplateDeprecationWarning')).toBe(true);
+ });
+ });
+
describe('form validation', () => {
beforeEach(async () => {
await act(async () => {
@@ -150,6 +175,11 @@ describe(' ', () => {
expect(find('stepTitle').text()).toEqual('Component templates (optional)');
});
+ it(`doesn't render the deprecated legacy index template warning`, () => {
+ const { exists } = testBed;
+ expect(exists('legacyIndexTemplateDeprecationWarning')).toBe(false);
+ });
+
it('should list the available component templates', () => {
const {
actions: {
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts
index 355bbc12f94fc..01aeba31770db 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts
@@ -306,6 +306,7 @@ export type TestSubjects =
| 'indexPatternsField'
| 'indexPatternsWarning'
| 'indexPatternsWarningDescription'
+ | 'legacyIndexTemplateDeprecationWarning'
| 'mappingsEditorFieldEdit'
| 'mockCodeEditor'
| 'mockComboBox'
diff --git a/x-pack/plugins/index_management/public/application/components/index_templates/index.ts b/x-pack/plugins/index_management/public/application/components/index_templates/index.ts
index a9131bab70551..d460175543ac5 100644
--- a/x-pack/plugins/index_management/public/application/components/index_templates/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/index_templates/index.ts
@@ -5,4 +5,12 @@
* 2.0.
*/
-export * from './simulate_template';
+export {
+ SimulateTemplateFlyoutContent,
+ simulateTemplateFlyoutProps,
+ SimulateTemplateProps,
+ SimulateTemplate,
+ SimulateTemplateFilters,
+} from './simulate_template';
+
+export { LegacyIndexTemplatesDeprecation } from './legacy_index_template_deprecation';
diff --git a/x-pack/plugins/index_management/public/application/components/index_templates/legacy_index_template_deprecation.tsx b/x-pack/plugins/index_management/public/application/components/index_templates/legacy_index_template_deprecation.tsx
new file mode 100644
index 0000000000000..6fbea1760f3a4
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/index_templates/legacy_index_template_deprecation.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { EuiCallOut, EuiLink } from '@elastic/eui';
+import { ScopedHistory } from 'kibana/public';
+import { reactRouterNavigate } from '../../../shared_imports';
+import { documentationService } from '../../services/documentation';
+
+interface Props {
+ history?: ScopedHistory;
+ showCta?: boolean;
+}
+
+export const LegacyIndexTemplatesDeprecation: React.FunctionComponent = ({
+ history,
+ showCta,
+}) => {
+ return (
+
+ {showCta && history && (
+
+
+
+
+ ),
+ learnMoreLink: (
+
+ {i18n.translate(
+ 'xpack.idxMgmt.home.legacyIndexTemplatesDeprecation.ctaLearnMoreLinkText',
+ {
+ defaultMessage: 'learn more.',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ )}
+
+ {!showCta && (
+
+ {i18n.translate('xpack.idxMgmt.home.legacyIndexTemplatesDeprecation.learnMoreLinkText', {
+ defaultMessage: 'Learn more.',
+ })}
+
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
index ef9cde30907f0..54160141827d0 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
@@ -9,24 +9,26 @@ import React, { useState, useCallback, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiSpacer, EuiButton } from '@elastic/eui';
+import { ScopedHistory } from 'kibana/public';
import { TemplateDeserialized } from '../../../../common';
import { serializers, Forms, GlobalFlyout } from '../../../shared_imports';
+import {
+ CommonWizardSteps,
+ StepSettingsContainer,
+ StepMappingsContainer,
+ StepAliasesContainer,
+} from '../shared';
+import { documentationService } from '../../services/documentation';
import { SectionError } from '../section_error';
import {
SimulateTemplateFlyoutContent,
SimulateTemplateProps,
simulateTemplateFlyoutProps,
SimulateTemplateFilters,
+ LegacyIndexTemplatesDeprecation,
} from '../index_templates';
import { StepLogisticsContainer, StepComponentContainer, StepReviewContainer } from './steps';
-import {
- CommonWizardSteps,
- StepSettingsContainer,
- StepMappingsContainer,
- StepAliasesContainer,
-} from '../shared';
-import { documentationService } from '../../services/documentation';
const { stripEmptyFields } = serializers;
const { FormWizard, FormWizardStep } = Forms;
@@ -38,6 +40,7 @@ interface Props {
clearSaveError: () => void;
isSaving: boolean;
saveError: any;
+ history?: ScopedHistory;
isLegacy?: boolean;
defaultValue?: TemplateDeserialized;
isEditing?: boolean;
@@ -98,6 +101,7 @@ export const TemplateForm = ({
saveError,
clearSaveError,
onSave,
+ history,
}: Props) => {
const [wizardContent, setWizardContent] = useState | null>(null);
const { addContent: addContentToGlobalFlyout, closeFlyout } = useGlobalFlyout();
@@ -283,12 +287,20 @@ export const TemplateForm = ({
);
};
+ const isLegacyIndexTemplate = indexTemplate._kbnMeta.isLegacy === true;
+
return (
<>
{/* Form header */}
{title}
-
+
+
+ {isLegacyIndexTemplate && (
+
+ )}
+
+
defaultValue={wizardDefaultValue}
@@ -311,7 +323,7 @@ export const TemplateForm = ({
/>
- {indexTemplate._kbnMeta.isLegacy !== true && (
+ {!isLegacyIndexTemplate && (
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
index b820bf559fb74..8b756be535ed2 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export * from './template_type_indicator';
+export { TemplateTypeIndicator } from './template_type_indicator';
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
index ecd41455d3249..b8b5a8e3c7d1a 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
@@ -24,7 +24,13 @@ import {
import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants';
import { TemplateListItem } from '../../../../../common';
-import { SectionError, SectionLoading, Error } from '../../../components';
+import { attemptToURIDecode } from '../../../../shared_imports';
+import {
+ SectionError,
+ SectionLoading,
+ Error,
+ LegacyIndexTemplatesDeprecation,
+} from '../../../components';
import { useLoadIndexTemplates } from '../../../services/api';
import { documentationService } from '../../../services/documentation';
import { useServices } from '../../../app_context';
@@ -34,11 +40,10 @@ import {
getTemplateCloneLink,
} from '../../../services/routing';
import { getIsLegacyFromQueryParams } from '../../../lib/index_templates';
+import { FilterListButton, Filters } from '../components';
import { TemplateTable } from './template_table';
import { TemplateDetails } from './template_details';
import { LegacyTemplateTable } from './legacy_templates/template_table';
-import { FilterListButton, Filters } from '../components';
-import { attemptToURIDecode } from '../../../../shared_imports';
type FilterName = 'managed' | 'cloudManaged' | 'system';
interface MatchParams {
@@ -130,7 +135,7 @@ export const TemplateList: React.FunctionComponent
-
+
+
+
+
+
+
+
0 && renderLegacyTemplatesTable()}
);
}
diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
index 37df44d175771..36bff298e345b 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
@@ -9,6 +9,7 @@ import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui';
+import { ScopedHistory } from 'kibana/public';
import { TemplateDeserialized } from '../../../../common';
import { TemplateForm, SectionLoading, SectionError, Error } from '../../components';
@@ -114,6 +115,7 @@ export const TemplateClone: React.FunctionComponent
);
}
diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
index 32b6ce7181bfd..310807aeef38f 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
@@ -11,10 +11,11 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui';
import { useLocation } from 'react-router-dom';
import { parse } from 'query-string';
+import { ScopedHistory } from 'kibana/public';
+import { TemplateDeserialized } from '../../../../common';
import { TemplateForm } from '../../components';
import { breadcrumbService } from '../../services/breadcrumbs';
-import { TemplateDeserialized } from '../../../../common';
import { saveTemplate } from '../../services/api';
import { getTemplateDetailsLink } from '../../services/routing';
@@ -76,6 +77,7 @@ export const TemplateCreate: React.FunctionComponent = ({ h
saveError={saveError}
clearSaveError={clearSaveError}
isLegacy={isLegacy}
+ history={history as ScopedHistory}
/>
diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
index 716f85e5ff1c4..f4ffe97931a24 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
@@ -9,14 +9,15 @@ import React, { useEffect, useState, Fragment } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui';
+import { ScopedHistory } from 'kibana/public';
import { TemplateDeserialized } from '../../../../common';
+import { attemptToURIDecode } from '../../../shared_imports';
import { breadcrumbService } from '../../services/breadcrumbs';
import { useLoadIndexTemplate, updateTemplate } from '../../services/api';
import { getTemplateDetailsLink } from '../../services/routing';
import { SectionLoading, SectionError, TemplateForm, Error } from '../../components';
import { getIsLegacyFromQueryParams } from '../../lib/index_templates';
-import { attemptToURIDecode } from '../../../shared_imports';
interface MatchParams {
name: string;
@@ -154,6 +155,7 @@ export const TemplateEdit: React.FunctionComponent
);
diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json
index c0d567ef83ced..ec1b11c90f7a3 100644
--- a/x-pack/plugins/infra/kibana.json
+++ b/x-pack/plugins/infra/kibana.json
@@ -11,9 +11,10 @@
"dataEnhanced",
"visTypeTimeseries",
"alerting",
- "triggersActionsUi"
+ "triggersActionsUi",
+ "observability"
],
- "optionalPlugins": ["ml", "observability", "home", "embeddable"],
+ "optionalPlugins": ["ml", "home", "embeddable"],
"server": true,
"ui": true,
"configPath": ["xpack", "infra"],
diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx
index 3b9193db65e1d..41867053c3a0f 100644
--- a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx
+++ b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx
@@ -134,7 +134,13 @@ export const MetricsAlertDropdown = () => {
panelPaddingSize="none"
anchorPosition="downLeft"
button={
-
+
}
diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx
index 7cd6295cdcf40..66c77fbf875a4 100644
--- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx
+++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx
@@ -83,7 +83,13 @@ export const AlertDropdown = () => {
+
}
diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx
index afbd6ffa8b5f7..e44a747aa07e7 100644
--- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx
@@ -11,7 +11,7 @@ import { EuiFlexGroup, EuiSpacer, EuiText, EuiLoadingContent } from '@elastic/eu
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities';
-import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
+import { SubscriptionSplashPrompt } from '../../../components/subscription_splash_content';
import { AlertPreview } from '../../common';
import {
METRIC_ANOMALY_ALERT_TYPE_ID,
@@ -185,7 +185,7 @@ export const Expression: React.FC = (props) => {
}, [metadata, derivedIndexPattern, defaultExpression, source, space]); // eslint-disable-line react-hooks/exhaustive-deps
if (isLoadingMLCapabilities) return ;
- if (!hasInfraMLCapabilities) return ;
+ if (!hasInfraMLCapabilities) return ;
return (
// https://github.com/elastic/kibana/issues/89506
diff --git a/x-pack/plugins/infra/public/apps/common_styles.ts b/x-pack/plugins/infra/public/apps/common_styles.ts
index be12c6cdc937f..68c820d538ca9 100644
--- a/x-pack/plugins/infra/public/apps/common_styles.ts
+++ b/x-pack/plugins/infra/public/apps/common_styles.ts
@@ -5,10 +5,15 @@
* 2.0.
*/
+import { APP_WRAPPER_CLASS } from '../../../../../src/core/public';
+
export const CONTAINER_CLASSNAME = 'infra-container-element';
-export const prepareMountElement = (element: HTMLElement) => {
- // Ensure the element we're handed from application mounting is assigned a class
- // for our index.scss styles to apply to.
- element.classList.add(CONTAINER_CLASSNAME);
+export const prepareMountElement = (element: HTMLElement, testSubject?: string) => {
+ // Ensure all wrapping elements have the APP_WRAPPER_CLASS so that the KinanaPageTemplate works as expected
+ element.classList.add(APP_WRAPPER_CLASS);
+
+ if (testSubject) {
+ element.setAttribute('data-test-subj', testSubject);
+ }
};
diff --git a/x-pack/plugins/infra/public/apps/logs_app.tsx b/x-pack/plugins/infra/public/apps/logs_app.tsx
index 61082efe43647..b512b5ce4a176 100644
--- a/x-pack/plugins/infra/public/apps/logs_app.tsx
+++ b/x-pack/plugins/infra/public/apps/logs_app.tsx
@@ -27,7 +27,7 @@ export const renderApp = (
) => {
const storage = new Storage(window.localStorage);
- prepareMountElement(element);
+ prepareMountElement(element, 'infraLogsPage');
ReactDOM.render(
{
const storage = new Storage(window.localStorage);
- prepareMountElement(element);
+ prepareMountElement(element, 'infraMetricsPage');
ReactDOM.render(
)}
- {uiCapabilities?.infrastructure?.show && (
-
- )}
{uiCapabilities?.infrastructure?.show && (
)}
diff --git a/x-pack/plugins/infra/public/assets/anomaly_chart_minified.svg b/x-pack/plugins/infra/public/assets/anomaly_chart_minified.svg
deleted file mode 100644
index dd1b39248bba2..0000000000000
--- a/x-pack/plugins/infra/public/assets/anomaly_chart_minified.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx b/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx
index 264428e44a44a..c61a567ac73b1 100644
--- a/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx
+++ b/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx
@@ -7,8 +7,7 @@
import { EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
-
-import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
+import { PageTemplate } from '../page_template';
interface NoIndicesProps {
message: string;
@@ -17,15 +16,16 @@ interface NoIndicesProps {
'data-test-subj'?: string;
}
-export const NoIndices: React.FC = ({ actions, message, title, ...rest }) => (
- {title}}
- body={{message}
}
- actions={actions}
- {...rest}
- />
-);
-
-const CenteredEmptyPrompt = euiStyled(EuiEmptyPrompt)`
- align-self: center;
-`;
+// Represents a fully constructed page, including page template.
+export const NoIndices: React.FC = ({ actions, message, title, ...rest }) => {
+ return (
+
+ {title}}
+ body={{message}
}
+ actions={actions}
+ {...rest}
+ />
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/components/error_page.tsx b/x-pack/plugins/infra/public/components/error_page.tsx
index 184901b4fdd9b..da6716ddc7f72 100644
--- a/x-pack/plugins/infra/public/components/error_page.tsx
+++ b/x-pack/plugins/infra/public/components/error_page.tsx
@@ -5,20 +5,10 @@
* 2.0.
*/
-import {
- EuiButton,
- EuiCallOut,
- EuiFlexGroup,
- EuiFlexItem,
- EuiPageBody,
- EuiPageContent,
- EuiPageContentBody,
- EuiSpacer,
-} from '@elastic/eui';
+import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
-import { euiStyled } from '../../../../../src/plugins/kibana_react/common';
-import { FlexPage } from './page';
+import { PageTemplate } from './page_template';
interface Props {
detailedMessage?: React.ReactNode;
@@ -26,51 +16,40 @@ interface Props {
shortMessage: React.ReactNode;
}
-export const ErrorPage: React.FC = ({ detailedMessage, retry, shortMessage }) => (
-
-
- = ({ detailedMessage, retry, shortMessage }) => {
+ return (
+
+
+ }
>
-
-
- }
- >
-
- {shortMessage}
- {retry ? (
-
-
-
-
-
- ) : null}
-
- {detailedMessage ? (
- <>
-
- {detailedMessage}
- >
- ) : null}
-
-
-
-
-
-);
-
-const MinimumPageContent = euiStyled(EuiPageContent)`
- min-width: 50vh;
-`;
+
+ {shortMessage}
+ {retry ? (
+
+
+
+
+
+ ) : null}
+
+ {detailedMessage ? (
+ <>
+
+ {detailedMessage}
+ >
+ ) : null}
+
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/components/loading_page.tsx b/x-pack/plugins/infra/public/components/loading_page.tsx
index 755511374b75f..2b2859707a20d 100644
--- a/x-pack/plugins/infra/public/components/loading_page.tsx
+++ b/x-pack/plugins/infra/public/components/loading_page.tsx
@@ -5,34 +5,38 @@
* 2.0.
*/
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
- EuiPageBody,
- EuiPageContent,
-} from '@elastic/eui';
+import { EuiEmptyPrompt, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { ReactNode } from 'react';
-
-import { FlexPage } from './page';
+import { PageTemplate } from './page_template';
interface LoadingPageProps {
message?: ReactNode;
'data-test-subj'?: string;
}
+// Represents a fully constructed page, including page template.
export const LoadingPage = ({
message,
'data-test-subj': dataTestSubj = 'loadingPage',
-}: LoadingPageProps) => (
-
-
-
-
-
- {message}
+}: LoadingPageProps) => {
+ return (
+
+
+
+ );
+};
+
+export const LoadingPrompt = ({ message }: LoadingPageProps) => {
+ return (
+
+
+
+
+ {message}
-
-
-
-);
+ }
+ />
+ );
+};
diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts
index db5a996c604fc..9ca08c69cf600 100644
--- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts
+++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts
@@ -5,8 +5,6 @@
* 2.0.
*/
-export * from './setup_page';
-
export * from './initial_configuration_step';
export * from './process_step';
diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_page.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_page.tsx
deleted file mode 100644
index a998d0c304a5e..0000000000000
--- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_page.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import {
- CommonProps,
- EuiPage,
- EuiPageBody,
- EuiPageContent,
- EuiPageContentBody,
- EuiPageContentHeader,
- EuiPageContentHeaderSection,
- EuiTitle,
-} from '@elastic/eui';
-import React from 'react';
-
-import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
-
-export const LogAnalysisSetupPage: React.FunctionComponent = ({
- children,
- ...rest
-}) => {
- return (
-
-
-
- {children}
-
-
-
- );
-};
-
-export const LogAnalysisSetupPageHeader: React.FunctionComponent = ({ children }) => (
-
-
-
- {children}
-
-
-
-);
-
-export const LogAnalysisSetupPageContent = EuiPageContentBody;
-
-// !important due to https://github.com/elastic/eui/issues/2232
-const LogEntryRateSetupPageContent = euiStyled(EuiPageContent)`
- max-width: 768px !important;
-`;
-
-const LogEntryRateSetupPage = euiStyled(EuiPage)`
- height: 100%;
-`;
diff --git a/x-pack/plugins/infra/public/components/logging/log_datepicker.tsx b/x-pack/plugins/infra/public/components/logging/log_datepicker.tsx
index b146da53caf6f..4f396ca7da495 100644
--- a/x-pack/plugins/infra/public/components/logging/log_datepicker.tsx
+++ b/x-pack/plugins/infra/public/components/logging/log_datepicker.tsx
@@ -6,7 +6,7 @@
*/
import React, { useCallback } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiButtonEmpty } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
interface LogDatepickerProps {
@@ -49,24 +49,19 @@ export const LogDatepicker: React.FC = ({
{isStreaming ? (
-
+
-
+
) : (
-
+
-
+
)}
diff --git a/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx b/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx
index 8ea35fd8f259f..6c757f7383a06 100644
--- a/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx
+++ b/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx
@@ -5,14 +5,7 @@
* 2.0.
*/
-import {
- EuiButton,
- EuiButtonEmpty,
- EuiCallOut,
- EuiEmptyPrompt,
- EuiPageTemplate,
- EuiSpacer,
-} from '@elastic/eui';
+import { EuiButton, EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { SavedObjectNotFound } from '../../../../../../src/plugins/kibana_utils/common';
@@ -22,6 +15,7 @@ import {
ResolveLogSourceConfigurationError,
} from '../../../common/log_sources';
import { useLinkProps } from '../../hooks/use_link_props';
+import { LogsPageTemplate } from '../../pages/logs/page_template';
export const LogSourceErrorPage: React.FC<{
errors: Error[];
@@ -30,7 +24,7 @@ export const LogSourceErrorPage: React.FC<{
const settingsLinkProps = useLinkProps({ app: 'logs', pathname: '/settings' });
return (
-
+
,
]}
/>
-
+
);
};
diff --git a/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx b/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx
deleted file mode 100644
index 966b91537d3bd..0000000000000
--- a/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import React from 'react';
-import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
-
-interface AppNavigationProps {
- 'aria-label': string;
- children: React.ReactNode;
-}
-
-export const AppNavigation = ({ 'aria-label': label, children }: AppNavigationProps) => (
-
-
- {children}
-
-
-);
-
-const Nav = euiStyled.nav`
- background: ${(props) => props.theme.eui.euiColorEmptyShade};
- border-bottom: ${(props) => props.theme.eui.euiBorderThin};
- padding: ${(props) => `${props.theme.eui.euiSizeS} ${props.theme.eui.euiSizeL}`};
- .euiTabs {
- padding-left: 3px;
- margin-left: -3px;
- };
-`;
diff --git a/x-pack/plugins/infra/public/components/navigation/routed_tabs.tsx b/x-pack/plugins/infra/public/components/navigation/routed_tabs.tsx
deleted file mode 100644
index 2a5ffcd826e7c..0000000000000
--- a/x-pack/plugins/infra/public/components/navigation/routed_tabs.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { EuiLink, EuiTab, EuiTabs } from '@elastic/eui';
-import React from 'react';
-import { Route } from 'react-router-dom';
-
-import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
-import { useLinkProps } from '../../hooks/use_link_props';
-import { LinkDescriptor } from '../../hooks/use_link_props';
-
-interface TabConfig {
- title: string | React.ReactNode;
-}
-
-type TabConfiguration = TabConfig & LinkDescriptor;
-
-interface RoutedTabsProps {
- tabs: TabConfiguration[];
-}
-
-const noop = () => {};
-
-export const RoutedTabs = ({ tabs }: RoutedTabsProps) => {
- return (
-
- {tabs.map((tab) => {
- return ;
- })}
-
- );
-};
-
-const Tab = ({ title, pathname, app }: TabConfiguration) => {
- const linkProps = useLinkProps({ app, pathname });
- return (
- {
- return (
-
-
-
- {title}
-
-
-
- );
- }}
- />
- );
-};
-
-const TabContainer = euiStyled.div`
- .euiLink {
- color: inherit !important;
- }
-`;
diff --git a/x-pack/plugins/infra/public/components/page_template.tsx b/x-pack/plugins/infra/public/components/page_template.tsx
new file mode 100644
index 0000000000000..1a10a6cd831b9
--- /dev/null
+++ b/x-pack/plugins/infra/public/components/page_template.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { useKibanaContextForPlugin } from '../hooks/use_kibana';
+import type { LazyObservabilityPageTemplateProps } from '../../../observability/public';
+
+export const PageTemplate: React.FC = (pageTemplateProps) => {
+ const {
+ services: {
+ observability: {
+ navigation: { PageTemplate: Template },
+ },
+ },
+ } = useKibanaContextForPlugin();
+
+ return ;
+};
diff --git a/x-pack/plugins/infra/public/components/subscription_splash_content.tsx b/x-pack/plugins/infra/public/components/subscription_splash_content.tsx
index a6477dfc7d172..5d7f28e7d2f84 100644
--- a/x-pack/plugins/infra/public/components/subscription_splash_content.tsx
+++ b/x-pack/plugins/infra/public/components/subscription_splash_content.tsx
@@ -7,28 +7,30 @@
import React, { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
-import {
- EuiPage,
- EuiPageBody,
- EuiPageContent,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiTitle,
- EuiText,
- EuiButton,
- EuiButtonEmpty,
- EuiImage,
-} from '@elastic/eui';
+import { EuiText, EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
-import { euiStyled, EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
import { HttpStart } from '../../../../../src/core/public';
import { useTrialStatus } from '../hooks/use_trial_status';
-import { LoadingPage } from '../components/loading_page';
+import { LoadingPrompt } from '../components/loading_page';
+import { PageTemplate } from './page_template';
+import type { LazyObservabilityPageTemplateProps } from '../../../observability/public';
+
+const loadingMessage = i18n.translate('xpack.infra.ml.splash.loadingMessage', {
+ defaultMessage: 'Checking license...',
+});
+
+export const SubscriptionSplashPage: React.FC = (
+ templateProps
+) => {
+ return (
+
+
+
+ );
+};
-export const SubscriptionSplashContent: React.FC = () => {
+export const SubscriptionSplashPrompt: React.FC = () => {
const { services } = useKibana<{ http: HttpStart }>();
const { loadState, isTrialAvailable, checkTrialAvailability } = useTrialStatus();
@@ -37,13 +39,7 @@ export const SubscriptionSplashContent: React.FC = () => {
}, [checkTrialAvailability]);
if (loadState === 'pending') {
- return (
-
- );
+ return ;
}
const canStartTrial = isTrialAvailable && loadState === 'resolved';
@@ -102,74 +98,15 @@ export const SubscriptionSplashContent: React.FC = () => {
}
return (
-
-
-
-
-
-
-
- {title}
-
-
-
- {description}
-
-
- {cta}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {title}}
+ body={
+
+ {description}
+
+ }
+ actions={cta}
+ />
);
};
-
-const SubscriptionPage = euiStyled(EuiPage)`
- height: 100%
-`;
-
-const SubscriptionPageContent = euiStyled(EuiPageContent)`
- max-width: 768px !important;
-`;
-
-const SubscriptionPageFooter = euiStyled.div`
- background: ${(props) => props.theme.eui.euiColorLightestShade};
- margin: 0 -${(props) => props.theme.eui.paddingSizes.l} -${(props) =>
- props.theme.eui.paddingSizes.l};
- padding: ${(props) => props.theme.eui.paddingSizes.l};
-`;
diff --git a/x-pack/plugins/infra/public/components/toolbar_panel.ts b/x-pack/plugins/infra/public/components/toolbar_panel.ts
deleted file mode 100644
index d94e7faa0eabf..0000000000000
--- a/x-pack/plugins/infra/public/components/toolbar_panel.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { FunctionComponent } from 'react';
-import { EuiPanel } from '@elastic/eui';
-import { StyledComponent } from 'styled-components';
-import { EuiTheme, euiStyled } from '../../../../../src/plugins/kibana_react/common';
-
-// The return type of this component needs to be specified because the inferred
-// return type depends on types that are not exported from EUI. You get a TS4023
-// error if the return type is not specified.
-export const ToolbarPanel: StyledComponent = euiStyled(EuiPanel).attrs(
- () => ({
- grow: false,
- paddingSize: 'none',
- })
-)`
- border-top: none;
- border-right: none;
- border-left: none;
- border-radius: 0;
- padding: ${(props) => `12px ${props.theme.eui.paddingSizes.m}`};
-`;
diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts
index ea1567d6056f1..6304471e818fa 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts
@@ -21,6 +21,7 @@ interface RequestArgs {
jobOverrides?: SetupMlModuleJobOverrides[];
datafeedOverrides?: SetupMlModuleDatafeedOverrides[];
query?: object;
+ useDedicatedIndex?: boolean;
}
export const callSetupMlModuleAPI = async (requestArgs: RequestArgs, fetch: HttpHandler) => {
@@ -34,6 +35,7 @@ export const callSetupMlModuleAPI = async (requestArgs: RequestArgs, fetch: Http
jobOverrides = [],
datafeedOverrides = [],
query,
+ useDedicatedIndex = false,
} = requestArgs;
const response = await fetch(`/api/ml/modules/setup/${moduleId}`, {
@@ -48,6 +50,7 @@ export const callSetupMlModuleAPI = async (requestArgs: RequestArgs, fetch: Http
jobOverrides,
datafeedOverrides,
query,
+ useDedicatedIndex,
})
),
});
@@ -78,6 +81,7 @@ const setupMlModuleRequestParamsRT = rt.intersection([
startDatafeed: rt.boolean,
jobOverrides: rt.array(setupMlModuleJobOverridesRT),
datafeedOverrides: rt.array(setupMlModuleDatafeedOverridesRT),
+ useDedicatedIndex: rt.boolean,
}),
rt.exact(
rt.partial({
diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_categories/module_descriptor.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_categories/module_descriptor.ts
index af2bd1802042a..6823ed173a740 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_categories/module_descriptor.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_categories/module_descriptor.ts
@@ -124,6 +124,7 @@ const setUpModule = async (
jobOverrides,
datafeedOverrides,
query,
+ useDedicatedIndex: true,
},
fetch
);
diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_rate/module_descriptor.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_rate/module_descriptor.ts
index 9704afd80e9ea..c4c939d0ebb9d 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_rate/module_descriptor.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_rate/module_descriptor.ts
@@ -116,6 +116,7 @@ const setUpModule = async (
jobOverrides,
datafeedOverrides,
query,
+ useDedicatedIndex: true,
},
fetch
);
diff --git a/x-pack/plugins/infra/public/index.scss b/x-pack/plugins/infra/public/index.scss
index 990092c792b14..e69de29bb2d1d 100644
--- a/x-pack/plugins/infra/public/index.scss
+++ b/x-pack/plugins/infra/public/index.scss
@@ -1,16 +0,0 @@
-/* Infra plugin styles */
-
-.infra-container-element {
- background-color: $euiColorEmptyShade;
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- align-items: flex-start;
- flex: 1 0 auto;
- overflow-x: hidden;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
-}
diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx
index 91f42509d493a..f9c80edd2c199 100644
--- a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx
+++ b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx
@@ -5,12 +5,12 @@
* 2.0.
*/
-import { render } from '@testing-library/react';
+import { render, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { httpServiceMock } from 'src/core/public/mocks';
-import { KibanaContextProvider } from 'src/plugins/kibana_react/public';
+import { KibanaContextProvider, KibanaPageTemplate } from 'src/plugins/kibana_react/public';
import { useLogSource } from '../../containers/logs/log_source';
import {
createLoadedUseLogSourceMock,
@@ -28,6 +28,11 @@ const renderRoutes = (routes: React.ReactElement) => {
data: {
indexPatterns: {},
},
+ observability: {
+ navigation: {
+ PageTemplate: KibanaPageTemplate,
+ },
+ },
};
const renderResult = render(
@@ -193,7 +198,7 @@ describe('LinkToLogsPage component', () => {
expect(searchParams.get('logPosition')).toEqual(null);
});
- it('renders a loading page while loading the source configuration', () => {
+ it('renders a loading page while loading the source configuration', async () => {
useLogSourceMock.mockImplementation(createLoadingUseLogSourceMock());
const { history, queryByTestId } = renderRoutes(
@@ -203,8 +208,9 @@ describe('LinkToLogsPage component', () => {
);
history.push('/link-to/host-logs/HOST_NAME');
-
- expect(queryByTestId('nodeLoadingPage-host')).not.toBeEmpty();
+ await waitFor(() => {
+ expect(queryByTestId('nodeLoadingPage-host')).not.toBeEmpty();
+ });
});
});
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page.tsx
index 6c816db4f5660..64dbcbdfe2258 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page.tsx
@@ -7,7 +7,6 @@
import { EuiErrorBoundary } from '@elastic/eui';
import React from 'react';
-import { ColumnarPage } from '../../../components/page';
import { LogEntryCategoriesPageContent } from './page_content';
import { LogEntryCategoriesPageProviders } from './page_providers';
@@ -15,9 +14,7 @@ export const LogEntryCategoriesPage = () => {
return (
-
-
-
+
);
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx
index 1762caed14a67..462b8b2f9dc3e 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx
@@ -18,11 +18,17 @@ import {
LogAnalysisSetupFlyout,
useLogAnalysisSetupFlyoutStateContext,
} from '../../../components/logging/log_analysis_setup/setup_flyout';
-import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
+import { SubscriptionSplashPage } from '../../../components/subscription_splash_content';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { LogEntryCategoriesResultsContent } from './page_results_content';
import { LogEntryCategoriesSetupContent } from './page_setup_content';
+import { LogsPageTemplate } from '../page_template';
+import type { LazyObservabilityPageTemplateProps } from '../../../../../observability/public';
+
+const logCategoriesTitle = i18n.translate('xpack.infra.logs.logCategoriesTitle', {
+ defaultMessage: 'Categories',
+});
export const LogEntryCategoriesPageContent = () => {
const {
@@ -45,9 +51,20 @@ export const LogEntryCategoriesPageContent = () => {
}, [fetchJobStatus, hasLogAnalysisReadCapabilities]);
if (!hasLogAnalysisCapabilites) {
- return ;
+ return (
+
+ );
} else if (!hasLogAnalysisReadCapabilities) {
- return ;
+ return (
+
+
+
+ );
} else if (setupStatus.type === 'initializing') {
return (
{
/>
);
} else if (setupStatus.type === 'unknown') {
- return ;
+ return (
+
+
+
+ );
} else if (isJobStatusWithResults(jobStatus['log-entry-categories-count'])) {
return (
<>
-
+
>
);
} else if (!hasLogAnalysisSetupCapabilities) {
- return ;
+ return (
+
+
+
+ );
} else {
return (
<>
-
+
+
+
>
);
@@ -78,3 +108,20 @@ export const LogEntryCategoriesPageContent = () => {
};
const allowedSetupModules = ['logs_ui_categories' as const];
+
+const CategoriesPageTemplate: React.FC = ({
+ children,
+ ...rest
+}) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx
index 1206e5c365441..7098f457117d3 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx
@@ -6,7 +6,7 @@
*/
import datemath from '@elastic/datemath';
-import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiSuperDatePicker } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
@@ -26,19 +26,31 @@ import {
useLogEntryCategoriesResultsUrlState,
} from './use_log_entry_categories_results_url_state';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis/log_analysis_capabilities';
+import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
+import { LogsPageTemplate } from '../page_template';
+import { RecreateJobButton } from '../../../components/logging/log_analysis_setup/create_job_button';
+import { AnalyzeInMlButton } from '../../../components/logging/log_analysis_results';
+import { useMlHref, ML_PAGES } from '../../../../../ml/public';
+import { DatasetsSelector } from '../../../components/logging/log_analysis_results/datasets_selector';
const JOB_STATUS_POLLING_INTERVAL = 30000;
interface LogEntryCategoriesResultsContentProps {
onOpenSetup: () => void;
+ pageTitle: string;
}
export const LogEntryCategoriesResultsContent: React.FunctionComponent = ({
onOpenSetup,
+ pageTitle,
}) => {
useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_results' });
useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_results', delay: 15000 });
+ const {
+ services: { ml, http },
+ } = useKibanaContextForPlugin();
+
const { hasLogAnalysisSetupCapabilities } = useLogAnalysisCapabilitiesContext();
const {
@@ -178,17 +190,48 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent
-
+ ,
+ ,
+ ],
+ }}
+ >
-
+
+
+
-
-
-
+
-
+
);
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx
index cf6f31811c123..18adfaad03ef8 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx
@@ -7,13 +7,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui';
-
-import {
- LogAnalysisSetupPage,
- LogAnalysisSetupPageContent,
- LogAnalysisSetupPageHeader,
-} from '../../../components/logging/log_analysis_setup';
+import { EuiText, EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { useTrackPageview } from '../../../../../observability/public';
interface LogEntryCategoriesSetupContentProps {
@@ -27,14 +21,17 @@ export const LogEntryCategoriesSetupContent: React.FunctionComponent
-
-
-
-
+
+
+
+ }
+ body={
-
+ }
+ actions={
-
-
+ }
+ />
);
};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx
index f5b94bce74e67..0aabc570a89e8 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx
@@ -5,97 +5,35 @@
* 2.0.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer, EuiTitle } from '@elastic/eui';
-import moment from 'moment';
+import { EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { LogEntryCategory } from '../../../../../../common/log_analysis';
import { TimeRange } from '../../../../../../common/time';
-import { BetaBadge } from '../../../../../components/beta_badge';
import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper';
-import { RecreateJobButton } from '../../../../../components/logging/log_analysis_setup/create_job_button';
-import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results';
-import { DatasetsSelector } from '../../../../../components/logging/log_analysis_results/datasets_selector';
import { TopCategoriesTable } from './top_categories_table';
import { SortOptions, ChangeSortOptions } from '../../use_log_entry_categories_results';
-import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana';
-import { useMlHref, ML_PAGES } from '../../../../../../../ml/public';
export const TopCategoriesSection: React.FunctionComponent<{
- availableDatasets: string[];
- hasSetupCapabilities: boolean;
- isLoadingDatasets?: boolean;
isLoadingTopCategories?: boolean;
jobId: string;
- onChangeDatasetSelection: (datasets: string[]) => void;
- onRequestRecreateMlJob: () => void;
- selectedDatasets: string[];
sourceId: string;
timeRange: TimeRange;
topCategories: LogEntryCategory[];
sortOptions: SortOptions;
changeSortOptions: ChangeSortOptions;
}> = ({
- availableDatasets,
- hasSetupCapabilities,
- isLoadingDatasets = false,
isLoadingTopCategories = false,
jobId,
- onChangeDatasetSelection,
- onRequestRecreateMlJob,
- selectedDatasets,
sourceId,
timeRange,
topCategories,
sortOptions,
changeSortOptions,
}) => {
- const {
- services: { ml, http },
- } = useKibanaContextForPlugin();
-
- const analyzeInMlLink = useMlHref(ml, http.basePath.get(), {
- page: ML_PAGES.ANOMALY_EXPLORER,
- pageState: {
- jobIds: [jobId],
- timeRange: {
- from: moment(timeRange.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
- to: moment(timeRange.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
- mode: 'absolute',
- },
- },
- });
-
return (
<>
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
-
-
-
-
-
}
@@ -113,10 +51,6 @@ export const TopCategoriesSection: React.FunctionComponent<{
);
};
-const title = i18n.translate('xpack.infra.logs.logEntryCategories.topCategoriesSectionTitle', {
- defaultMessage: 'Log message categories',
-});
-
const loadingAriaLabel = i18n.translate(
'xpack.infra.logs.logEntryCategories.topCategoriesSectionLoadingAriaLabel',
{ defaultMessage: 'Loading message categories' }
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx
index 24116a8dbbf3b..ff4cba731b616 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx
@@ -7,7 +7,6 @@
import { EuiErrorBoundary } from '@elastic/eui';
import React from 'react';
-import { ColumnarPage } from '../../../components/page';
import { LogEntryRatePageContent } from './page_content';
import { LogEntryRatePageProviders } from './page_providers';
@@ -15,9 +14,7 @@ export const LogEntryRatePage = () => {
return (
-
-
-
+
);
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx
index 061a2ba0acc1d..ea60d073c2311 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx
@@ -19,15 +19,21 @@ import {
LogAnalysisSetupFlyout,
useLogAnalysisSetupFlyoutStateContext,
} from '../../../components/logging/log_analysis_setup/setup_flyout';
-import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
+import { SubscriptionSplashPage } from '../../../components/subscription_splash_content';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { LogEntryRateResultsContent } from './page_results_content';
import { LogEntryRateSetupContent } from './page_setup_content';
+import { LogsPageTemplate } from '../page_template';
+import type { LazyObservabilityPageTemplateProps } from '../../../../../observability/public';
const JOB_STATUS_POLLING_INTERVAL = 30000;
+const anomaliesTitle = i18n.translate('xpack.infra.logs.anomaliesPageTitle', {
+ defaultMessage: 'Anomalies',
+});
+
export const LogEntryRatePageContent = memo(() => {
const {
hasLogAnalysisCapabilites,
@@ -83,9 +89,20 @@ export const LogEntryRatePageContent = memo(() => {
}, JOB_STATUS_POLLING_INTERVAL);
if (!hasLogAnalysisCapabilites) {
- return ;
+ return (
+
+ );
} else if (!hasLogAnalysisReadCapabilities) {
- return ;
+ return (
+
+
+
+ );
} else if (
logEntryCategoriesSetupStatus.type === 'initializing' ||
logEntryRateSetupStatus.type === 'initializing'
@@ -101,25 +118,52 @@ export const LogEntryRatePageContent = memo(() => {
logEntryCategoriesSetupStatus.type === 'unknown' ||
logEntryRateSetupStatus.type === 'unknown'
) {
- return ;
+ return (
+
+
+
+ );
} else if (
isJobStatusWithResults(logEntryCategoriesJobStatus['log-entry-categories-count']) ||
isJobStatusWithResults(logEntryRateJobStatus['log-entry-rate'])
) {
return (
<>
-
+
>
);
} else if (!hasLogAnalysisSetupCapabilities) {
- return ;
+ return (
+
+ ;
+
+ );
} else {
return (
<>
-
+
+
+
>
);
}
});
+
+const AnomaliesPageTemplate: React.FC = ({
+ children,
+ ...rest
+}) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
index 0741423fd3886..2d833c87c1e25 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
@@ -5,16 +5,14 @@
* 2.0.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker } from '@elastic/eui';
import moment from 'moment';
import { stringify } from 'query-string';
import React, { useCallback, useMemo } from 'react';
import { encode, RisonValue } from 'rison-node';
import type { Query } from '../../../../../../../src/plugins/data/public';
-import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { useTrackPageview } from '../../../../../observability/public';
-import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { TimeKey } from '../../../../common/time';
import {
CategoryJobNoticesSection,
@@ -32,6 +30,9 @@ import { AnomaliesResults } from './sections/anomalies';
import { useDatasetFiltering } from './use_dataset_filtering';
import { useLogEntryAnomaliesResults } from './use_log_entry_anomalies_results';
import { useLogAnalysisResultsUrlState } from './use_log_entry_rate_results_url_state';
+import { isJobStatusWithResults } from '../../../../common/log_analysis';
+import { LogsPageTemplate } from '../page_template';
+import { ManageJobsButton } from '../../../components/logging/log_analysis_setup/manage_jobs_button';
export const SORT_DEFAULTS = {
direction: 'desc' as const,
@@ -42,9 +43,12 @@ export const PAGINATION_DEFAULTS = {
pageSize: 25,
};
-export const LogEntryRateResultsContent: React.FunctionComponent = () => {
+export const LogEntryRateResultsContent: React.FunctionComponent<{
+ pageTitle: string;
+}> = ({ pageTitle }) => {
useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results' });
useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results', delay: 15000 });
+
const navigateToApp = useKibana().services.application?.navigateToApp;
const { sourceId } = useLogSourceContext();
@@ -188,76 +192,76 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
);
return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ],
+ }}
+ >
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{isLogEntryFlyoutOpen ? (
{
sourceId={sourceId}
/>
) : null}
- >
+
);
};
-
-// This is needed due to the flex-basis: 100% !important; rule that
-// kicks in on small screens via media queries breaking when using direction="column"
-export const ResultsContentPage = euiStyled(EuiPage)`
- flex: 1 0 0%;
-
- .euiFlexGroup--responsive > .euiFlexItem {
- flex-basis: auto !important;
- }
-`;
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx
index 1754d18596165..9c7b393e4c200 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx
@@ -7,13 +7,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui';
-
-import {
- LogAnalysisSetupPage,
- LogAnalysisSetupPageContent,
- LogAnalysisSetupPageHeader,
-} from '../../../components/logging/log_analysis_setup';
+import { EuiText, EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { useTrackPageview } from '../../../../../observability/public';
interface LogEntryRateSetupContentProps {
@@ -27,14 +21,17 @@ export const LogEntryRateSetupContent: React.FunctionComponent
-
-
-
-
+
+
+
+ }
+ body={
-
+ }
+ actions={
-
-
+ }
+ />
);
};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx
index 3bc206e9ad7bb..7741414f79046 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx
@@ -10,7 +10,6 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
- EuiTitle,
EuiLoadingSpinner,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -18,7 +17,6 @@ import React from 'react';
import { TimeRange } from '../../../../../../common/time/time_range';
import { AnomaliesSwimlaneVisualisation } from './anomalies_swimlane_visualisation';
import { AnomaliesTable } from './table';
-import { ManageJobsButton } from '../../../../../components/logging/log_analysis_setup/manage_jobs_button';
import {
ChangePaginationOptions,
ChangeSortOptions,
@@ -36,7 +34,6 @@ export const AnomaliesResults: React.FunctionComponent<{
isLoadingAnomaliesResults: boolean;
anomalies: LogEntryAnomalies;
timeRange: TimeRange;
- onViewModuleList: () => void;
page: Page;
fetchNextPage?: FetchNextPage;
fetchPreviousPage?: FetchPreviousPage;
@@ -50,7 +47,6 @@ export const AnomaliesResults: React.FunctionComponent<{
}> = ({
isLoadingAnomaliesResults,
timeRange,
- onViewModuleList,
anomalies,
changeSortOptions,
sortOptions,
@@ -65,17 +61,6 @@ export const AnomaliesResults: React.FunctionComponent<{
}) => {
return (
<>
-
-
-
- {title}
-
-
-
-
-
-
-
{
const uiCapabilities = useKibana().services.application?.capabilities;
@@ -65,15 +63,25 @@ export const LogsPageContent: React.FunctionComponent = () => {
pathname: '/settings',
};
+ const settingsLinkProps = useLinkProps({
+ app: 'logs',
+ pathname: 'settings',
+ });
+
return (
-
+ <>
{setHeaderActionMenu && (
-
+
+
+
+ {settingsTabTitle}
+
+
@@ -101,13 +109,6 @@ export const LogsPageContent: React.FunctionComponent = () => {
]}
readOnlyBadge={!uiCapabilities?.logs?.save}
/>
-
-
-
-
-
-
-
@@ -117,7 +118,7 @@ export const LogsPageContent: React.FunctionComponent = () => {
-
+ >
);
};
diff --git a/x-pack/plugins/infra/public/pages/logs/page_template.tsx b/x-pack/plugins/infra/public/pages/logs/page_template.tsx
new file mode 100644
index 0000000000000..42ca992e9402d
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/logs/page_template.tsx
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
+import type { LazyObservabilityPageTemplateProps } from '../../../../observability/public';
+
+export const LogsPageTemplate: React.FC = (
+ pageTemplateProps
+) => {
+ const {
+ services: {
+ observability: {
+ navigation: { PageTemplate },
+ },
+ },
+ } = useKibanaContextForPlugin();
+
+ return ;
+};
diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx
index b295a392c8df9..180949572b086 100644
--- a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx
@@ -10,9 +10,6 @@ import {
EuiErrorBoundary,
EuiFlexGroup,
EuiFlexItem,
- EuiPage,
- EuiPageBody,
- EuiPageContentBody,
EuiPanel,
EuiSpacer,
} from '@elastic/eui';
@@ -29,6 +26,11 @@ import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'
import { NameConfigurationPanel } from './name_configuration_panel';
import { LogSourceConfigurationFormErrors } from './source_configuration_form_errors';
import { useLogSourceConfigurationFormState } from './source_configuration_form_state';
+import { LogsPageTemplate } from '../page_template';
+
+const settingsTitle = i18n.translate('xpack.infra.logs.settingsTitle', {
+ defaultMessage: 'Settings',
+});
export const LogsSettingsPage = () => {
const uiCapabilities = useKibana().services.application?.capabilities;
@@ -84,102 +86,104 @@ export const LogsSettingsPage = () => {
return (
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {sourceConfigurationFormElement.validity.validity === 'invalid' ? (
+ <>
+
-
-
-
-
-
-
-
-
-
-
-
- {sourceConfigurationFormElement.validity.validity === 'invalid' ? (
- <>
-
-
- >
- ) : null}
-
- {isWriteable && (
-
- {isLoading ? (
-
-
-
- Loading
-
-
-
- ) : (
- <>
-
-
- {
- sourceConfigurationFormElement.resetValue();
- }}
- >
-
-
-
-
-
-
-
-
-
- >
- )}
-
+ >
+ ) : null}
+
+ {isWriteable && (
+
+ {isLoading ? (
+
+
+
+ Loading
+
+
+
+ ) : (
+ <>
+
+
+ {
+ sourceConfigurationFormElement.resetValue();
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ >
)}
-
-
-
-
+
+ )}
+
+
);
};
diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx
index 10aff0a08d25b..99b66d2d4ab7b 100644
--- a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx
@@ -8,7 +8,6 @@
import { EuiErrorBoundary } from '@elastic/eui';
import React from 'react';
import { useTrackPageview } from '../../../../../observability/public';
-import { ColumnarPage } from '../../../components/page';
import { StreamPageContent } from './page_content';
import { StreamPageHeader } from './page_header';
import { LogsPageProviders } from './page_providers';
@@ -19,10 +18,8 @@ export const StreamPage = () => {
return (
-
-
-
-
+
+
);
diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx
index 5ff07e713233a..527dc79726044 100644
--- a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx
@@ -6,11 +6,19 @@
*/
import React from 'react';
+import { i18n } from '@kbn/i18n';
import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { LogsPageLogsContent } from './page_logs_content';
import { LogsPageNoIndicesContent } from './page_no_indices_content';
+import { LogsPageTemplate } from '../page_template';
+import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
+import { APP_WRAPPER_CLASS } from '../../../../../../../src/core/public';
+
+const streamTitle = i18n.translate('xpack.infra.logs.streamPageTitle', {
+ defaultMessage: 'Stream',
+});
export const StreamPageContent: React.FunctionComponent = () => {
const {
@@ -27,8 +35,31 @@ export const StreamPageContent: React.FunctionComponent = () => {
} else if (hasFailedLoading) {
return ;
} else if (sourceStatus?.logIndexStatus !== 'missing') {
- return ;
+ return (
+
+
+
+
+
+ );
} else {
return ;
}
};
+
+// This is added to facilitate a full height layout whereby the
+// inner container will set it's own height and be scrollable.
+// The "fullHeight" prop won't help us as it only applies to certain breakpoints.
+export const LogStreamPageWrapper = euiStyled.div`
+ .euiPage .euiPageContentBody {
+ display: flex;
+ flex-direction: column;
+ flex: 1 0 auto;
+ width: 100%;
+ height: 100%;
+ }
+`;
diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx
index 165a5a03bcc6e..2af290b816689 100644
--- a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx
@@ -5,7 +5,8 @@
* 2.0.
*/
-import React, { useCallback, useContext, useEffect, useMemo } from 'react';
+import { EuiSpacer } from '@elastic/eui';
+import React, { useContext, useCallback, useMemo, useEffect } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import type { Query } from '../../../../../../../src/plugins/data/public';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
@@ -208,6 +209,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
+
{isFlyoutOpen ? (
{
} = useContext(LogPositionState.Context);
return (
-
-
+
+
{
/>
-
+
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx
index 819c764bfb7ba..e52d1e90d7efd 100644
--- a/x-pack/plugins/infra/public/pages/metrics/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx
@@ -15,8 +15,6 @@ import { IIndexPattern } from 'src/plugins/data/common';
import { MetricsSourceConfigurationProperties } from '../../../common/metrics_sources';
import { DocumentTitle } from '../../components/document_title';
import { HelpCenterContent } from '../../components/help_center_content';
-import { RoutedTabs } from '../../components/navigation/routed_tabs';
-import { ColumnarPage } from '../../components/page';
import { Header } from '../../components/header';
import {
MetricsExplorerOptionsContainer,
@@ -27,8 +25,8 @@ import { WithSource } from '../../containers/with_source';
import { Source } from '../../containers/metrics_source';
import { MetricsExplorerPage } from './metrics_explorer';
import { SnapshotPage } from './inventory_view';
+import { MetricDetail } from './metric_detail';
import { MetricsSettingsPage } from './settings';
-import { AppNavigation } from '../../components/navigation/app_navigation';
import { SourceLoadingPage } from '../../components/source_loading_page';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options';
@@ -42,6 +40,7 @@ import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabi
import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout';
import { HeaderMenuPortal } from '../../../../observability/public';
import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider';
+import { useLinkProps } from '../../hooks/use_link_props';
const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLabel', {
defaultMessage: 'Add data',
@@ -51,8 +50,17 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
const uiCapabilities = useKibana().services.application?.capabilities;
const { setHeaderActionMenu } = useContext(HeaderActionMenuContext);
+ const settingsTabTitle = i18n.translate('xpack.infra.metrics.settingsTabTitle', {
+ defaultMessage: 'Settings',
+ });
+
const kibana = useKibana();
+ const settingsLinkProps = useLinkProps({
+ app: 'metrics',
+ pathname: 'settings',
+ });
+
return (
@@ -61,123 +69,84 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
-
-
+
-
+
- {setHeaderActionMenu && (
-
-
-
-
-
-
-
-
-
-
- {ADD_DATA_LABEL}
-
-
-
-
- )}
-
-
-
-
-
- {/** !! Need to be kept in sync with the deepLinks in x-pack/plugins/infra/public/plugin.ts */}
-
+ {setHeaderActionMenu && (
+
+
+
+
+ {settingsTabTitle}
+
+
+
+
+
+
+
+
+
+
+ {ADD_DATA_LABEL}
+
-
+
+ )}
-
-
- (
-
- {({ configuration, createDerivedIndexPattern }) => (
-
-
- {configuration ? (
-
- ) : (
-
- )}
-
- )}
-
- )}
- />
-
-
-
+
+
+
+ (
+
+ {({ configuration, createDerivedIndexPattern }) => (
+
+
+ {configuration ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ )}
+ />
+
+
+
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx
index 6176dce9a8782..fe0fbeecf8408 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx
@@ -74,8 +74,8 @@ const BottomActionContainer = euiStyled.div<{ isOpen: boolean; outerWidth: numbe
right: 0;
transition: transform ${TRANSITION_MS}ms;
transform: translateY(${(props) => (props.isOpen ? 0 : '224px')});
- width: ${(props) => props.outerWidth}px;
-`;
+ width: ${(props) => props.outerWidth + 34}px;
+`; // Additional width comes from the padding on the EuiPageBody and inner nodes container
const BottomActionTopBar = euiStyled(EuiFlexGroup).attrs({
justifyContent: 'spaceBetween',
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx
index 145302b6cf8eb..7cb75e488583a 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx
@@ -10,17 +10,19 @@ import React from 'react';
import { WaffleTimeControls } from './waffle/waffle_time_controls';
import { SearchBar } from './search_bar';
-import { ToolbarPanel } from '../../../../components/toolbar_panel';
export const FilterBar = () => (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx
index 5438209ae9c6b..d2cd4f87a5342 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx
@@ -51,6 +51,8 @@ export const AnomalyDetectionFlyout = () => {
return (
<>
{
});
if (!hasInfraMLCapabilities) {
- return ;
+ return ;
} else if (!hasInfraMLReadCapabilities) {
return ;
} else if (hostSetupStatus.type === 'initializing' || k8sSetupStatus.type === 'initializing') {
return (
- {
);
return (
-
-
+
+
{
const uiCapabilities = useKibana().services.application?.capabilities;
@@ -48,75 +54,94 @@ export const SnapshotPage = () => {
return (
-
-
- i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', {
- defaultMessage: '{previousTitle} | Inventory',
- values: {
- previousTitle,
- },
- })
- }
- />
- {isLoading && !source ? (
-
- ) : metricIndicesExist ? (
- <>
-
-
+ i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', {
+ defaultMessage: '{previousTitle} | Inventory',
+ values: {
+ previousTitle,
+ },
+ })
+ }
+ />
+ {isLoading && !source ? (
+
+ ) : metricIndicesExist ? (
+ <>
+
+
-
-
- >
- ) : hasFailedLoadingSource ? (
-
- ) : (
-
+
+
+
+
+
+
+ >
+ ) : hasFailedLoadingSource ? (
+
+ ) : (
+
+
+
+ {i18n.translate('xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', {
+ defaultMessage: 'View setup instructions',
+ })}
+
+
+ {uiCapabilities?.infrastructure?.configureSource ? (
-
- {i18n.translate(
- 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel',
- {
- defaultMessage: 'View setup instructions',
- }
- )}
-
+ {i18n.translate('xpack.infra.configureSourceActionLabel', {
+ defaultMessage: 'Change source configuration',
+ })}
+
- {uiCapabilities?.infrastructure?.configureSource ? (
-
-
- {i18n.translate('xpack.infra.configureSourceActionLabel', {
- defaultMessage: 'Change source configuration',
- })}
-
-
- ) : null}
-
- }
- data-test-subj="noMetricsIndicesPrompt"
- />
- )}
-
+ ) : null}
+
+ }
+ data-test-subj="noMetricsIndicesPrompt"
+ />
+ )}
);
};
+
+// This is added to facilitate a full height layout whereby the
+// inner container will set it's own height and be scrollable.
+// The "fullHeight" prop won't help us as it only applies to certain breakpoints.
+export const InventoryPageWrapper = euiStyled.div`
+ .euiPage .euiPageContentBody {
+ display: flex;
+ flex-direction: column;
+ flex: 1 0 auto;
+ width: 100%;
+ height: 100%;
+ }
+`;
diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx
index 631ec676f70f5..24b58a4c9a44e 100644
--- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx
@@ -6,27 +6,19 @@
*/
import React, { useCallback, useEffect } from 'react';
-import {
- EuiPage,
- EuiPageBody,
- EuiPageHeader,
- EuiPageHeaderSection,
- EuiHideFor,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { InfraTimerangeInput } from '../../../../../common/http_api/snapshot_api';
import { InventoryMetric, InventoryItemType } from '../../../../../common/inventory_models/types';
import { useNodeDetails } from '../hooks/use_node_details';
import { MetricsSideNav } from './side_nav';
-import { AutoSizer } from '../../../../components/auto_sizer';
import { MetricsTimeControls } from './time_controls';
import { SideNavContext, NavItem } from '../lib/side_nav_context';
import { PageBody } from './page_body';
-import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
import { MetricsTimeInput } from '../hooks/use_metrics_time';
import { InfraMetadata } from '../../../../../common/http_api/metadata_api';
import { PageError } from './page_error';
import { MetadataContext } from '../containers/metadata_context';
+import { MetricsPageTemplate } from '../../page_template';
interface Props {
name: string;
@@ -72,69 +64,47 @@ export const NodeDetailsPage = (props: Props) => {
}
return (
-
-
-
- {({ bounds: { width = 0 } }) => {
- const w = width ? `${width}px` : `100%`;
- return (
-
-
-
-
-
-
-
- {props.name}
-
-
-
-
-
-
-
-
- 0 && props.isAutoReloading ? false : loading}
- refetch={refetch}
- type={props.nodeType}
- metrics={metrics}
- onChangeRangeTime={props.setTimeRange}
- isLiveStreaming={props.isAutoReloading}
- stopLiveStreaming={() => props.setAutoReload(false)}
- />
-
-
-
-
- );
- }}
-
-
+ ,
+ ],
+ }}
+ >
+
+
+
+
+
+
+
+ 0 && props.isAutoReloading ? false : loading}
+ refetch={refetch}
+ type={props.nodeType}
+ metrics={metrics}
+ onChangeRangeTime={props.setTimeRange}
+ isLiveStreaming={props.isAutoReloading}
+ stopLiveStreaming={() => props.setAutoReload(false)}
+ />
+
+
+
+
+
);
};
-
-const MetricsDetailsPageColumn = euiStyled.div`
- flex: 1 0 0%;
- display: flex;
- flex-direction: column;
-`;
-
-const MetricsTitleTimeRangeContainer = euiStyled.div`
- display: flex;
- flex-flow: row wrap;
- justify-content: space-between;
-`;
diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx
index 478e85b1b4e27..a0ef8ab40ad90 100644
--- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx
@@ -7,9 +7,7 @@
import { EuiHideFor, EuiPageSideBar, EuiShowFor, EuiSideNav } from '@elastic/eui';
import React, { useState, useCallback } from 'react';
-import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
import { NavItem } from '../lib/side_nav_context';
-
interface Props {
loading: boolean;
name: string;
@@ -32,22 +30,15 @@ export const MetricsSideNav = ({ loading, name, items }: Props) => {
isOpenOnMobile={isOpenOnMobile}
/>
);
+
return (
-
-
- {content}
+ <>
+
+ {content}
- {mobileContent}
-
+
+ {mobileContent}
+
+ >
);
};
-
-const SideNavContainer = euiStyled.div`
- position: fixed;
- z-index: 1;
- height: 88vh;
- padding-left: 16px;
- margin-left: -16px;
- overflow-y: auto;
- overflow-x: hidden;
-`;
diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx
index 13fa5cf1f0667..a447989530727 100644
--- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx
@@ -7,14 +7,9 @@
import { i18n } from '@kbn/i18n';
import React, { useContext, useState } from 'react';
-import {
- euiStyled,
- EuiTheme,
- withTheme,
-} from '../../../../../../../src/plugins/kibana_react/common';
+import { EuiTheme, withTheme } from '../../../../../../../src/plugins/kibana_react/common';
import { DocumentTitle } from '../../../components/document_title';
import { Header } from '../../../components/header';
-import { ColumnarPage, PageContent } from '../../../components/page';
import { withMetricPageProviders } from './page_providers';
import { useMetadata } from './hooks/use_metadata';
import { Source } from '../../../containers/metrics_source';
@@ -26,11 +21,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'
import { InventoryItemType } from '../../../../common/inventory_models/types';
import { useMetricsTimeContext } from './hooks/use_metrics_time';
import { useLinkProps } from '../../../hooks/use_link_props';
-
-const DetailPageContent = euiStyled(PageContent)`
- overflow: auto;
- background-color: ${(props) => props.theme.eui.euiColorLightestShade};
-`;
+import { MetricsPageTemplate } from '../page_template';
interface Props {
theme: EuiTheme | undefined;
@@ -49,6 +40,7 @@ export const MetricDetail = withMetricPageProviders(
const nodeType = match.params.type as InventoryItemType;
const inventoryModel = findInventoryModel(nodeType);
const { sourceId } = useContext(Source.Context);
+
const {
timeRange,
parsedTimeRange,
@@ -95,18 +87,20 @@ export const MetricDetail = withMetricPageProviders(
if (metadataLoading && !filteredRequiredMetrics.length) {
return (
-
+
+
+
);
}
return (
-
+ <>
-
- {metadata ? (
-
- ) : null}
-
-
+ {metadata ? (
+
+ ) : null}
+ >
);
})
);
diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx
index e34cd1fd479b8..1b33546d3b68f 100644
--- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx
@@ -26,7 +26,6 @@ import { MetricsExplorerChartOptions as MetricsExplorerChartOptionsComponent } f
import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control';
import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting';
import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges';
-import { ToolbarPanel } from '../../../../components/toolbar_panel';
interface Props {
derivedIndexPattern: IIndexPattern;
@@ -60,7 +59,7 @@ export const MetricsExplorerToolbar = ({
const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges);
return (
-
+ <>
-
+ >
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx
index 0d1ac47812577..1ecadcac4e287 100644
--- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx
@@ -17,12 +17,17 @@ import { MetricsExplorerCharts } from './components/charts';
import { MetricsExplorerToolbar } from './components/toolbar';
import { useMetricsExplorerState } from './hooks/use_metric_explorer_state';
import { useSavedViewContext } from '../../../containers/saved_view/saved_view';
+import { MetricsPageTemplate } from '../page_template';
interface MetricsExplorerPageProps {
source: MetricsSourceConfigurationProperties;
derivedIndexPattern: IIndexPattern;
}
+const metricsExplorerTitle = i18n.translate('xpack.infra.metrics.metricsExplorerTitle', {
+ defaultMessage: 'Metrics Explorer',
+});
+
export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExplorerPageProps) => {
const {
loading,
@@ -73,43 +78,49 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl
})
}
/>
-
- {error ? (
-
- ) : (
-
+
- )}
+ {error ? (
+
+ ) : (
+
+ )}
+
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/page_template.tsx b/x-pack/plugins/infra/public/pages/metrics/page_template.tsx
new file mode 100644
index 0000000000000..c32133525e8e8
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/page_template.tsx
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
+import type { LazyObservabilityPageTemplateProps } from '../../../../observability/public';
+
+export const MetricsPageTemplate: React.FC = (
+ pageTemplateProps
+) => {
+ const {
+ services: {
+ observability: {
+ navigation: { PageTemplate },
+ },
+ },
+ } = useKibanaContextForPlugin();
+
+ return ;
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx
index 0579194464f4e..1066dddad6b5f 100644
--- a/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx
@@ -10,8 +10,6 @@ import {
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
- EuiPage,
- EuiPageBody,
EuiPanel,
EuiSpacer,
} from '@elastic/eui';
@@ -27,11 +25,15 @@ import { IndicesConfigurationPanel } from './indices_configuration_panel';
import { MLConfigurationPanel } from './ml_configuration_panel';
import { NameConfigurationPanel } from './name_configuration_panel';
import { useSourceConfigurationFormState } from './source_configuration_form_state';
-
+import { MetricsPageTemplate } from '../page_template';
interface SourceConfigurationSettingsProps {
shouldAllowEdit: boolean;
}
+const settingsTitle = i18n.translate('xpack.infra.metrics.settingsTitle', {
+ defaultMessage: 'Settings',
+});
+
export const SourceConfigurationSettings = ({
shouldAllowEdit,
}: SourceConfigurationSettingsProps) => {
@@ -84,126 +86,124 @@ export const SourceConfigurationSettings = ({
}
return (
- <>
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {hasInfraMLCapabilities && (
+ <>
-
- {hasInfraMLCapabilities && (
- <>
-
-
-
-
- >
- )}
- {errors.length > 0 ? (
- <>
-
-
- {errors.map((error, errorIndex) => (
- {error}
- ))}
-
-
-
- >
- ) : null}
+ >
+ )}
+ {errors.length > 0 ? (
+ <>
+
+
+ {errors.map((error, errorIndex) => (
+ {error}
+ ))}
+
+
-
- {isWriteable && (
-
- {isLoading ? (
-
-
-
- Loading
-
-
-
- ) : (
- <>
-
-
- {
- resetForm();
- }}
- >
-
-
-
-
-
-
-
-
-
- >
- )}
-
+ >
+ ) : null}
+
+
+ {isWriteable && (
+
+ {isLoading ? (
+
+
+
+ Loading
+
+
+
+ ) : (
+ <>
+
+
+ {
+ resetForm();
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ >
)}
-
-
-
- >
+
+ )}
+
+
);
};
diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts
index 9948976b01ea1..fd599aed5f890 100644
--- a/x-pack/plugins/infra/public/plugin.ts
+++ b/x-pack/plugins/infra/public/plugin.ts
@@ -7,6 +7,8 @@
import { i18n } from '@kbn/i18n';
import { AppMountParameters, PluginInitializerContext } from 'kibana/public';
+import { from } from 'rxjs';
+import { map } from 'rxjs/operators';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
import { createInventoryMetricAlertType } from './alerting/inventory';
@@ -36,19 +38,50 @@ export class Plugin implements InfraClientPluginClass {
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(getLogsAlertType());
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType());
- if (pluginsSetup.observability) {
- pluginsSetup.observability.dashboard.register({
- appName: 'infra_logs',
- hasData: getLogsHasDataFetcher(core.getStartServices),
- fetchData: getLogsOverviewDataFetcher(core.getStartServices),
- });
-
- pluginsSetup.observability.dashboard.register({
- appName: 'infra_metrics',
- hasData: createMetricsHasData(core.getStartServices),
- fetchData: createMetricsFetchData(core.getStartServices),
- });
- }
+ pluginsSetup.observability.dashboard.register({
+ appName: 'infra_logs',
+ hasData: getLogsHasDataFetcher(core.getStartServices),
+ fetchData: getLogsOverviewDataFetcher(core.getStartServices),
+ });
+
+ pluginsSetup.observability.dashboard.register({
+ appName: 'infra_metrics',
+ hasData: createMetricsHasData(core.getStartServices),
+ fetchData: createMetricsFetchData(core.getStartServices),
+ });
+
+ /** !! Need to be kept in sync with the deepLinks in x-pack/plugins/infra/public/plugin.ts */
+ pluginsSetup.observability.navigation.registerSections(
+ from(core.getStartServices()).pipe(
+ map(([{ application: { capabilities } }]) => [
+ ...(capabilities.logs.show
+ ? [
+ {
+ label: 'Logs',
+ sortKey: 200,
+ entries: [
+ { label: 'Stream', app: 'logs', path: '/stream' },
+ { label: 'Anomalies', app: 'logs', path: '/anomalies' },
+ { label: 'Categories', app: 'logs', path: '/log-categories' },
+ ],
+ },
+ ]
+ : []),
+ ...(capabilities.infrastructure.show
+ ? [
+ {
+ label: 'Metrics',
+ sortKey: 300,
+ entries: [
+ { label: 'Inventory', app: 'metrics', path: '/inventory' },
+ { label: 'Metrics Explorer', app: 'metrics', path: '/explorer' },
+ ],
+ },
+ ]
+ : []),
+ ])
+ )
+ );
pluginsSetup.embeddable.registerEmbeddableFactory(
LOG_STREAM_EMBEDDABLE,
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
index 76ebe21a51367..78e3f2dab0d1d 100644
--- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
@@ -162,7 +162,7 @@ describe(' ', () => {
const { exists, find } = testBed;
expect(exists('pipelineLoadError')).toBe(true);
- expect(find('pipelineLoadError').text()).toContain('Unable to load pipelines.');
+ expect(find('pipelineLoadError').text()).toContain('Unable to load pipelines');
});
});
});
diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx
index 1cca7a0721fbc..da8f74e1efae5 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx
@@ -6,7 +6,7 @@
*/
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageContent } from '@elastic/eui';
+import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui';
import React, { FunctionComponent } from 'react';
import { Router, Switch, Route } from 'react-router-dom';
@@ -19,7 +19,6 @@ import {
useAuthorizationContext,
WithPrivileges,
SectionLoading,
- NotAuthorizedSection,
} from '../shared_imports';
import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections';
@@ -61,35 +60,42 @@ export const App: FunctionComponent = () => {
{({ isLoading, hasPrivileges, privilegesMissing }) => {
if (isLoading) {
return (
-
-
-
+
+
+
+
+
);
}
if (!hasPrivileges) {
return (
-
-
+
+
+
+
}
- message={
-
+ body={
+
+
+
}
/>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx
index c51ed94cbc116..f68b64cc5f613 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx
@@ -8,6 +8,7 @@
import React, { FunctionComponent, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
+import { EuiPageContent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { SectionLoading, useKibana, attemptToURIDecode } from '../../../shared_imports';
@@ -45,12 +46,14 @@ export const PipelinesClone: FunctionComponent>
if (isLoading && isInitialRequest) {
return (
-
-
-
+
+
+
+
+
);
} else {
// We still show the create form even if we were not able to load the
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx
index eefc74e2dc6fa..5aa9205e1e1e5 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx
@@ -8,15 +8,7 @@
import React, { useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiPageBody,
- EuiPageContent,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonEmpty,
- EuiSpacer,
-} from '@elastic/eui';
+import { EuiPageHeader, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
import { getListPath } from '../../services/navigation';
import { Pipeline } from '../../../../common/types';
@@ -64,49 +56,43 @@ export const PipelinesCreate: React.FunctionComponent
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
+
+ >
);
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx
index 6011a36d292fa..ea47f4c9a25e9 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx
@@ -9,16 +9,14 @@ import React, { useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- EuiPageBody,
+ EuiPageHeader,
+ EuiEmptyPrompt,
EuiPageContent,
EuiSpacer,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
+ EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
-import { EuiCallOut } from '@elastic/eui';
import { Pipeline } from '../../../../common/types';
import { useKibana, SectionLoading, attemptToURIDecode } from '../../../shared_imports';
@@ -42,7 +40,9 @@ export const PipelinesEdit: React.FunctionComponent {
setIsSaving(true);
@@ -68,88 +68,92 @@ export const PipelinesEdit: React.FunctionComponent
-
-
+ return (
+
+
+
+
+
);
- } else if (error) {
- content = (
- <>
-
+
+
+
+
}
- color="danger"
- iconType="alert"
- data-test-subj="fetchPipelineError"
- >
- {error.message}
-
-
- >
+ body={{error.message}
}
+ actions={
+
+
+
+ }
+ />
+
);
- } else if (pipeline) {
- content = (
+ }
+
+ return (
+ <>
+
+
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
- );
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {content}
-
-
+ >
);
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
index 7d69bd3fb8cf3..9f401bca5431f 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
@@ -8,7 +8,7 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiEmptyPrompt, EuiLink, EuiPageBody, EuiPageContent, EuiButton } from '@elastic/eui';
+import { EuiEmptyPrompt, EuiLink, EuiPageContent, EuiButton } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { ScopedHistory } from 'kibana/public';
import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
@@ -20,46 +20,43 @@ export const EmptyList: FunctionComponent = () => {
const history = useHistory() as ScopedHistory;
return (
-
-
-
- {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', {
- defaultMessage: 'Start by creating a pipeline',
+
+
+ {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', {
+ defaultMessage: 'Start by creating a pipeline',
+ })}
+
+ }
+ body={
+
+ {' '}
+
+ {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptDocumentionLink', {
+ defaultMessage: 'Learn more',
})}
-
- }
- body={
-
-
-
-
- {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptDocumentionLink', {
- defaultMessage: 'Learn more',
- })}
-
-
- }
- actions={
-
- {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', {
- defaultMessage: 'Create a pipeline',
- })}
-
- }
- />
-
-
+
+
+ }
+ actions={
+
+ {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', {
+ defaultMessage: 'Create a pipeline',
+ })}
+
+ }
+ />
+
);
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
index 454747fe0870e..ae68cfcb399f0 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
@@ -12,16 +12,12 @@ import { Location } from 'history';
import { parse } from 'query-string';
import {
- EuiPageBody,
- EuiPageContent,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
+ EuiPageHeader,
EuiButtonEmpty,
- EuiCallOut,
- EuiLink,
+ EuiPageContent,
+ EuiEmptyPrompt,
+ EuiButton,
EuiSpacer,
- EuiText,
} from '@elastic/eui';
import { Pipeline } from '../../../../common/types';
@@ -81,33 +77,50 @@ export const PipelinesList: React.FunctionComponent = ({
history.push(getListPath());
};
- if (data && data.length === 0) {
- return ;
+ if (error) {
+ return (
+
+
+
+
+ }
+ body={{error.message}
}
+ actions={
+
+
+
+ }
+ />
+
+ );
}
- let content: React.ReactNode;
-
if (isLoading) {
- content = (
-
-
-
- );
- } else if (data?.length) {
- content = (
-
+ return (
+
+
+
+
+
);
}
+ if (data && data.length === 0) {
+ return ;
+ }
+
const renderFlyout = (): React.ReactNode => {
if (!showFlyout) {
return;
@@ -134,71 +147,47 @@ export const PipelinesList: React.FunctionComponent = ({
return (
<>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Error call out for pipeline table */}
- {error ? (
-
-
-
- ),
- }}
- />
- }
+
+
- ) : (
- content
- )}
-
-
+
+ }
+ description={
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
+
+
{renderFlyout()}
{pipelinesToDelete?.length > 0 ? (
& {
+ returnToOrigin: boolean;
+ dashboardId?: string | null;
+ onTitleDuplicate?: OnSaveProps['onTitleDuplicate'];
+ newDescription?: string;
+ newTags?: string[];
+};
export function App({
history,
@@ -48,26 +53,23 @@ export function App({
initialInput,
incomingState,
redirectToOrigin,
- redirectToDashboard,
setHeaderActionMenu,
initialContext,
}: LensAppProps) {
+ const lensAppServices = useKibana().services;
+
const {
data,
chrome,
- overlays,
uiSettings,
application,
- stateTransfer,
notifications,
- attributeService,
- savedObjectsClient,
savedObjectsTagging,
getOriginatingAppName,
// Temporarily required until the 'by value' paradigm is default.
dashboardFeatureFlag,
- } = useKibana().services;
+ } = lensAppServices;
const dispatch = useLensDispatch();
const dispatchSetState: DispatchSetState = useCallback(
@@ -205,150 +207,40 @@ export function App({
getIsByValueMode,
application,
chrome,
- initialInput,
appState.isLinkedToOriginatingApp,
appState.persistedDoc,
]);
- const tagsIds =
- appState.persistedDoc && savedObjectsTagging
- ? savedObjectsTagging.ui.getTagIdsFromReferences(appState.persistedDoc.references)
- : [];
-
- const runSave: RunSave = async (saveProps, options) => {
- if (!lastKnownDoc) {
- return;
- }
-
- let references = lastKnownDoc.references;
- if (savedObjectsTagging) {
- references = savedObjectsTagging.ui.updateTagsReferences(
- references,
- saveProps.newTags || tagsIds
- );
- }
-
- const docToSave = {
- ...getLastKnownDocWithoutPinnedFilters(lastKnownDoc)!,
- description: saveProps.newDescription,
- title: saveProps.newTitle,
- references,
- };
-
- // Required to serialize filters in by value mode until
- // https://github.com/elastic/kibana/issues/77588 is fixed
- if (getIsByValueMode()) {
- docToSave.state.filters.forEach((filter) => {
- if (typeof filter.meta.value === 'function') {
- delete filter.meta.value;
+ const runSave = (saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
+ return runSaveLensVisualization(
+ {
+ lastKnownDoc,
+ getIsByValueMode,
+ savedObjectsTagging,
+ initialInput,
+ redirectToOrigin,
+ persistedDoc: appState.persistedDoc,
+ onAppLeave,
+ redirectTo,
+ originatingApp: incomingState?.originatingApp,
+ ...lensAppServices,
+ },
+ saveProps,
+ options
+ ).then(
+ (newState) => {
+ if (newState) {
+ dispatchSetState(newState);
+ setIsSaveModalVisible(false);
}
- });
- }
-
- const originalInput = saveProps.newCopyOnSave ? undefined : initialInput;
- const originalSavedObjectId = (originalInput as LensByReferenceInput)?.savedObjectId;
- if (options.saveToLibrary) {
- try {
- await checkForDuplicateTitle(
- {
- id: originalSavedObjectId,
- title: docToSave.title,
- copyOnSave: saveProps.newCopyOnSave,
- lastSavedTitle: lastKnownDoc.title,
- getEsType: () => 'lens',
- getDisplayName: () =>
- i18n.translate('xpack.lens.app.saveModalType', {
- defaultMessage: 'Lens visualization',
- }),
- },
- saveProps.isTitleDuplicateConfirmed,
- saveProps.onTitleDuplicate,
- {
- savedObjectsClient,
- overlays,
- }
- );
- } catch (e) {
- // ignore duplicate title failure, user notified in save modal
- return;
- }
- }
- try {
- const newInput = (await attributeService.wrapAttributes(
- docToSave,
- options.saveToLibrary,
- originalInput
- )) as LensEmbeddableInput;
-
- if (saveProps.returnToOrigin && redirectToOrigin) {
- // disabling the validation on app leave because the document has been saved.
- onAppLeave((actions) => {
- return actions.default();
- });
- redirectToOrigin({ input: newInput, isCopied: saveProps.newCopyOnSave });
- return;
- } else if (saveProps.dashboardId && redirectToDashboard) {
- // disabling the validation on app leave because the document has been saved.
- onAppLeave((actions) => {
- return actions.default();
- });
- redirectToDashboard(newInput, saveProps.dashboardId);
- return;
- }
-
- notifications.toasts.addSuccess(
- i18n.translate('xpack.lens.app.saveVisualization.successNotificationText', {
- defaultMessage: `Saved '{visTitle}'`,
- values: {
- visTitle: docToSave.title,
- },
- })
- );
-
- if (
- attributeService.inputIsRefType(newInput) &&
- newInput.savedObjectId !== originalSavedObjectId
- ) {
- chrome.recentlyAccessed.add(
- getFullPath(newInput.savedObjectId),
- docToSave.title,
- newInput.savedObjectId
- );
-
- dispatchSetState({ isLinkedToOriginatingApp: false });
-
- setIsSaveModalVisible(false);
- // remove editor state so the connection is still broken after reload
- stateTransfer.clearEditorState(APP_ID);
-
- redirectTo(newInput.savedObjectId);
- return;
+ },
+ () => {
+ // error is handled inside the modal
+ // so ignoring it here
}
-
- const newDoc = {
- ...docToSave,
- ...newInput,
- };
-
- dispatchSetState({
- isLinkedToOriginatingApp: false,
- persistedDoc: newDoc,
- lastKnownDoc: newDoc,
- });
-
- setIsSaveModalVisible(false);
- } catch (e) {
- // eslint-disable-next-line no-console
- console.dir(e);
- trackUiEvent('save_failed');
- setIsSaveModalVisible(false);
- }
+ );
};
- const savingToLibraryPermitted = Boolean(
- appState.isSaveable && application.capabilities.visualize.save
- );
-
return (
<>
@@ -371,21 +263,24 @@ export function App({
/>
)}
- {
setIsSaveModalVisible(false);
}}
getAppNameFromId={() => getOriginatingAppName()}
lastKnownDoc={lastKnownDoc}
+ onAppLeave={onAppLeave}
+ persistedDoc={appState.persistedDoc}
+ initialInput={initialInput}
+ redirectTo={redirectTo}
+ redirectToOrigin={redirectToOrigin}
returnToOriginSwitchLabel={
getIsByValueMode() && initialInput
? i18n.translate('xpack.lens.app.updatePanel', {
@@ -419,20 +314,3 @@ const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({
/>
);
});
-
-function getLastKnownDocWithoutPinnedFilters(doc?: Document) {
- if (!doc) return undefined;
- const [pinnedFilters, appFilters] = partition(
- injectFilterReferences(doc.state?.filters || [], doc.references),
- esFilters.isFilterPinned
- );
- return pinnedFilters?.length
- ? {
- ...doc,
- state: {
- ...doc.state,
- filters: appFilters,
- },
- }
- : doc;
-}
diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx
index 3e56fbb2003cb..8e59f90c958f9 100644
--- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx
@@ -7,7 +7,7 @@
import React, { FC, useCallback } from 'react';
-import { AppMountParameters, CoreSetup } from 'kibana/public';
+import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { History } from 'history';
@@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n';
import { DashboardFeatureFlagConfig } from 'src/plugins/dashboard/public';
import { Provider } from 'react-redux';
-import { uniq, isEqual } from 'lodash';
+import { isEqual } from 'lodash';
import { EmbeddableEditorState } from 'src/plugins/embeddable/public';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
@@ -26,7 +26,7 @@ import { App } from './app';
import { EditorFrameStart } from '../types';
import { addHelpMenuToAppChrome } from '../help_menu_util';
import { LensPluginStartDependencies } from '../plugin';
-import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID, getFullPath } from '../../common';
+import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common';
import {
LensEmbeddableInput,
LensByReferenceInput,
@@ -44,41 +44,26 @@ import {
LensRootStore,
setState,
} from '../state_management';
-import { getAllIndexPatterns, getResolvedDateRange } from '../utils';
-import { injectFilterReferences } from '../persistence';
+import { getResolvedDateRange } from '../utils';
+import { getLastKnownDoc } from './save_modal_container';
-export async function mountApp(
- core: CoreSetup,
- params: AppMountParameters,
- mountProps: {
- createEditorFrame: EditorFrameStart['createInstance'];
- getByValueFeatureFlag: () => Promise;
- attributeService: () => Promise;
- getPresentationUtilContext: () => Promise;
- }
-) {
- const {
- createEditorFrame,
- getByValueFeatureFlag,
- attributeService,
- getPresentationUtilContext,
- } = mountProps;
- const [coreStart, startDependencies] = await core.getStartServices();
- const { data, navigation, embeddable, savedObjectsTagging } = startDependencies;
+export async function getLensServices(
+ coreStart: CoreStart,
+ startDependencies: LensPluginStartDependencies,
+ attributeService: () => Promise
+): Promise {
+ const { data, navigation, embeddable, savedObjectsTagging, usageCollection } = startDependencies;
- const instance = await createEditorFrame();
const storage = new Storage(localStorage);
const stateTransfer = embeddable?.getStateTransfer();
- const historyLocationState = params.history.location.state as HistoryLocationState;
const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID);
- const dashboardFeatureFlag = await getByValueFeatureFlag();
-
- const lensServices: LensAppServices = {
+ return {
data,
storage,
navigation,
stateTransfer,
+ usageCollection,
savedObjectsTagging,
attributeService: await attributeService(),
http: coreStart.http,
@@ -88,6 +73,8 @@ export async function mountApp(
application: coreStart.application,
notifications: coreStart.notifications,
savedObjectsClient: coreStart.savedObjects.client,
+ presentationUtil: startDependencies.presentationUtil,
+ dashboard: startDependencies.dashboard,
getOriginatingAppName: () => {
return embeddableEditorIncomingState?.originatingApp
? stateTransfer?.getAppNameFromId(embeddableEditorIncomingState.originatingApp)
@@ -95,8 +82,29 @@ export async function mountApp(
},
// Temporarily required until the 'by value' paradigm is default.
- dashboardFeatureFlag,
+ dashboardFeatureFlag: startDependencies.dashboard.dashboardFeatureFlagConfig,
};
+}
+
+export async function mountApp(
+ core: CoreSetup,
+ params: AppMountParameters,
+ mountProps: {
+ createEditorFrame: EditorFrameStart['createInstance'];
+ attributeService: () => Promise;
+ getPresentationUtilContext: () => Promise;
+ }
+) {
+ const { createEditorFrame, attributeService, getPresentationUtilContext } = mountProps;
+ const [coreStart, startDependencies] = await core.getStartServices();
+ const instance = await createEditorFrame();
+ const historyLocationState = params.history.location.state as HistoryLocationState;
+
+ const lensServices = await getLensServices(coreStart, startDependencies, attributeService);
+
+ const { stateTransfer, data, storage, dashboardFeatureFlag } = lensServices;
+
+ const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID);
addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks);
coreStart.chrome.docTitle.change(
@@ -130,23 +138,6 @@ export async function mountApp(
}
};
- const redirectToDashboard = (embeddableInput: LensEmbeddableInput, dashboardId: string) => {
- if (!lensServices.dashboardFeatureFlag.allowByValueEmbeddables) {
- throw new Error('redirectToDashboard called with by-value embeddables disabled');
- }
-
- const state = {
- input: embeddableInput,
- type: LENS_EMBEDDABLE_TYPE,
- };
-
- const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;
- stateTransfer.navigateToWithEmbeddablePackage('dashboards', {
- state,
- path,
- });
- };
-
const redirectToOrigin = (props?: RedirectToOriginProps) => {
if (!embeddableEditorIncomingState?.originatingApp) {
throw new Error('redirectToOrigin called without an originating app');
@@ -215,7 +206,6 @@ export async function mountApp(
initialInput={initialInput}
redirectTo={redirectCallback}
redirectToOrigin={redirectToOrigin}
- redirectToDashboard={redirectToDashboard}
onAppLeave={params.onAppLeave}
setHeaderActionMenu={params.setHeaderActionMenu}
history={props.history}
@@ -299,73 +289,45 @@ export function loadDocument(
}
lensStore.dispatch(setState({ isAppLoading: true }));
- attributeService
- .unwrapAttributes(initialInput)
- .then((attributes) => {
- if (!initialInput) {
- return;
- }
- const doc = {
- ...initialInput,
- ...attributes,
- type: LENS_EMBEDDABLE_TYPE,
- };
-
- if (attributeService.inputIsRefType(initialInput)) {
- chrome.recentlyAccessed.add(
- getFullPath(initialInput.savedObjectId),
- attributes.title,
- initialInput.savedObjectId
+ getLastKnownDoc({
+ initialInput,
+ attributeService,
+ data,
+ chrome,
+ notifications,
+ }).then(
+ (newState) => {
+ if (newState) {
+ const { doc, indexPatterns } = newState;
+ const currentSessionId = data.search.session.getSessionId();
+ lensStore.dispatch(
+ setState({
+ query: doc.state.query,
+ isAppLoading: false,
+ indexPatternsForTopNav: indexPatterns,
+ lastKnownDoc: doc,
+ searchSessionId:
+ dashboardFeatureFlag.allowByValueEmbeddables &&
+ Boolean(embeddableEditorIncomingState?.originatingApp) &&
+ !(initialInput as LensByReferenceInput)?.savedObjectId &&
+ currentSessionId
+ ? currentSessionId
+ : data.search.session.start(),
+ ...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
+ })
);
+ } else {
+ redirectCallback();
}
- const indexPatternIds = uniq(
- doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
- );
- getAllIndexPatterns(indexPatternIds, data.indexPatterns)
- .then(({ indexPatterns }) => {
- // Don't overwrite any pinned filters
- data.query.filterManager.setAppFilters(
- injectFilterReferences(doc.state.filters, doc.references)
- );
- const currentSessionId = data.search.session.getSessionId();
- lensStore.dispatch(
- setState({
- query: doc.state.query,
- isAppLoading: false,
- indexPatternsForTopNav: indexPatterns,
- lastKnownDoc: doc,
- searchSessionId:
- dashboardFeatureFlag.allowByValueEmbeddables &&
- Boolean(embeddableEditorIncomingState?.originatingApp) &&
- !(initialInput as LensByReferenceInput)?.savedObjectId &&
- currentSessionId
- ? currentSessionId
- : data.search.session.start(),
- ...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
- })
- );
- })
- .catch((e) => {
- lensStore.dispatch(
- setState({
- isAppLoading: false,
- })
- );
- redirectCallback();
- });
- })
- .catch((e) => {
+ },
+ () => {
lensStore.dispatch(
setState({
isAppLoading: false,
})
);
- notifications.toasts.addDanger(
- i18n.translate('xpack.lens.app.docLoadingError', {
- defaultMessage: 'Error loading saved document',
- })
- );
redirectCallback();
- });
+ }
+ );
}
diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx
new file mode 100644
index 0000000000000..a65c8e6732e44
--- /dev/null
+++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx
@@ -0,0 +1,413 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+import { ChromeStart, NotificationsStart } from 'kibana/public';
+import { i18n } from '@kbn/i18n';
+import { partition, uniq } from 'lodash';
+import { METRIC_TYPE } from '@kbn/analytics';
+import { SaveModal } from './save_modal';
+import { LensAppProps, LensAppServices } from './types';
+import type { SaveProps } from './app';
+import { Document, injectFilterReferences } from '../persistence';
+import { LensByReferenceInput, LensEmbeddableInput } from '../editor_frame_service/embeddable';
+import { LensAttributeService } from '../lens_attribute_service';
+import {
+ DataPublicPluginStart,
+ esFilters,
+ IndexPattern,
+} from '../../../../../src/plugins/data/public';
+import { APP_ID, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../common';
+import { getAllIndexPatterns } from '../utils';
+import { trackUiEvent } from '../lens_ui_telemetry';
+import { checkForDuplicateTitle } from '../../../../../src/plugins/saved_objects/public';
+import { LensAppState } from '../state_management';
+
+type ExtraProps = Pick &
+ Partial>;
+
+export type SaveModalContainerProps = {
+ isVisible: boolean;
+ originatingApp?: string;
+ persistedDoc?: Document;
+ lastKnownDoc?: Document;
+ returnToOriginSwitchLabel?: string;
+ onClose: () => void;
+ onSave?: () => void;
+ runSave?: (saveProps: SaveProps, options: { saveToLibrary: boolean }) => void;
+ isSaveable?: boolean;
+ getAppNameFromId?: () => string | undefined;
+ lensServices: LensAppServices;
+} & ExtraProps;
+
+export function SaveModalContainer({
+ returnToOriginSwitchLabel,
+ onClose,
+ onSave,
+ runSave,
+ isVisible,
+ persistedDoc,
+ originatingApp,
+ initialInput,
+ redirectTo,
+ redirectToOrigin,
+ getAppNameFromId = () => undefined,
+ isSaveable = true,
+ lastKnownDoc: initLastKnowDoc,
+ lensServices,
+}: SaveModalContainerProps) {
+ const [lastKnownDoc, setLastKnownDoc] = useState(initLastKnowDoc);
+
+ const {
+ attributeService,
+ notifications,
+ data,
+ chrome,
+ savedObjectsTagging,
+ application,
+ dashboardFeatureFlag,
+ } = lensServices;
+
+ useEffect(() => {
+ setLastKnownDoc(initLastKnowDoc);
+ }, [initLastKnowDoc]);
+
+ useEffect(() => {
+ async function loadLastKnownDoc() {
+ if (initialInput && isVisible) {
+ getLastKnownDoc({
+ data,
+ initialInput,
+ chrome,
+ notifications,
+ attributeService,
+ }).then((result) => {
+ if (result) setLastKnownDoc(result.doc);
+ });
+ }
+ }
+
+ loadLastKnownDoc();
+ }, [chrome, data, initialInput, notifications, attributeService, isVisible]);
+
+ const tagsIds =
+ persistedDoc && savedObjectsTagging
+ ? savedObjectsTagging.ui.getTagIdsFromReferences(persistedDoc.references)
+ : [];
+
+ const runLensSave = (saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
+ if (runSave) {
+ // inside lens, we use the function that's passed to it
+ runSave(saveProps, options);
+ } else {
+ if (attributeService && lastKnownDoc) {
+ runSaveLensVisualization(
+ {
+ ...lensServices,
+ lastKnownDoc,
+ initialInput,
+ attributeService,
+ redirectTo,
+ redirectToOrigin,
+ originatingApp,
+ getIsByValueMode: () => false,
+ onAppLeave: () => {},
+ },
+ saveProps,
+ options
+ ).then(() => {
+ onSave?.();
+ onClose();
+ });
+ }
+ }
+ };
+
+ const savingToLibraryPermitted = Boolean(isSaveable && application.capabilities.visualize.save);
+
+ return (
+ {
+ runLensSave(saveProps, options);
+ }}
+ onClose={onClose}
+ getAppNameFromId={getAppNameFromId}
+ lastKnownDoc={lastKnownDoc}
+ returnToOriginSwitchLabel={returnToOriginSwitchLabel}
+ />
+ );
+}
+
+const redirectToDashboard = ({
+ embeddableInput,
+ dashboardFeatureFlag,
+ dashboardId,
+ stateTransfer,
+}: {
+ embeddableInput: LensEmbeddableInput;
+ dashboardId: string;
+ dashboardFeatureFlag: LensAppServices['dashboardFeatureFlag'];
+ stateTransfer: LensAppServices['stateTransfer'];
+}) => {
+ if (!dashboardFeatureFlag.allowByValueEmbeddables) {
+ throw new Error('redirectToDashboard called with by-value embeddables disabled');
+ }
+
+ const state = {
+ input: embeddableInput,
+ type: LENS_EMBEDDABLE_TYPE,
+ };
+
+ const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;
+ stateTransfer.navigateToWithEmbeddablePackage('dashboards', {
+ state,
+ path,
+ });
+};
+
+export const runSaveLensVisualization = async (
+ props: {
+ lastKnownDoc?: Document;
+ getIsByValueMode: () => boolean;
+ persistedDoc?: Document;
+ originatingApp?: string;
+ } & ExtraProps &
+ LensAppServices,
+ saveProps: SaveProps,
+ options: { saveToLibrary: boolean }
+): Promise | undefined> => {
+ if (!props.lastKnownDoc) {
+ return;
+ }
+
+ const {
+ chrome,
+ initialInput,
+ originatingApp,
+ lastKnownDoc,
+ persistedDoc,
+ savedObjectsClient,
+ overlays,
+ notifications,
+ stateTransfer,
+ attributeService,
+ usageCollection,
+ savedObjectsTagging,
+ getIsByValueMode,
+ redirectToOrigin,
+ onAppLeave,
+ redirectTo,
+ dashboardFeatureFlag,
+ } = props;
+
+ const tagsIds =
+ persistedDoc && savedObjectsTagging
+ ? savedObjectsTagging.ui.getTagIdsFromReferences(persistedDoc.references)
+ : [];
+ if (usageCollection) {
+ usageCollection.reportUiCounter(originatingApp || 'visualize', METRIC_TYPE.CLICK, 'lens:save');
+ }
+
+ let references = lastKnownDoc.references;
+ if (savedObjectsTagging) {
+ references = savedObjectsTagging.ui.updateTagsReferences(
+ references,
+ saveProps.newTags || tagsIds
+ );
+ }
+
+ const docToSave = {
+ ...getLastKnownDocWithoutPinnedFilters(lastKnownDoc)!,
+ description: saveProps.newDescription,
+ title: saveProps.newTitle,
+ references,
+ };
+
+ // Required to serialize filters in by value mode until
+ // https://github.com/elastic/kibana/issues/77588 is fixed
+ if (getIsByValueMode()) {
+ docToSave.state.filters.forEach((filter) => {
+ if (typeof filter.meta.value === 'function') {
+ delete filter.meta.value;
+ }
+ });
+ }
+
+ const originalInput = saveProps.newCopyOnSave ? undefined : initialInput;
+ const originalSavedObjectId = (originalInput as LensByReferenceInput)?.savedObjectId;
+ if (options.saveToLibrary) {
+ try {
+ await checkForDuplicateTitle(
+ {
+ id: originalSavedObjectId,
+ title: docToSave.title,
+ copyOnSave: saveProps.newCopyOnSave,
+ lastSavedTitle: lastKnownDoc.title,
+ getEsType: () => 'lens',
+ getDisplayName: () =>
+ i18n.translate('xpack.lens.app.saveModalType', {
+ defaultMessage: 'Lens visualization',
+ }),
+ },
+ saveProps.isTitleDuplicateConfirmed,
+ saveProps.onTitleDuplicate,
+ {
+ savedObjectsClient,
+ overlays,
+ }
+ );
+ } catch (e) {
+ // ignore duplicate title failure, user notified in save modal
+ throw e;
+ }
+ }
+ try {
+ const newInput = (await attributeService.wrapAttributes(
+ docToSave,
+ options.saveToLibrary,
+ originalInput
+ )) as LensEmbeddableInput;
+
+ if (saveProps.returnToOrigin && redirectToOrigin) {
+ // disabling the validation on app leave because the document has been saved.
+ onAppLeave?.((actions) => {
+ return actions.default();
+ });
+ redirectToOrigin({ input: newInput, isCopied: saveProps.newCopyOnSave });
+ return;
+ } else if (saveProps.dashboardId) {
+ // disabling the validation on app leave because the document has been saved.
+ onAppLeave?.((actions) => {
+ return actions.default();
+ });
+ redirectToDashboard({
+ embeddableInput: newInput,
+ dashboardId: saveProps.dashboardId,
+ stateTransfer,
+ dashboardFeatureFlag,
+ });
+ return;
+ }
+
+ notifications.toasts.addSuccess(
+ i18n.translate('xpack.lens.app.saveVisualization.successNotificationText', {
+ defaultMessage: `Saved '{visTitle}'`,
+ values: {
+ visTitle: docToSave.title,
+ },
+ })
+ );
+
+ if (
+ attributeService.inputIsRefType(newInput) &&
+ newInput.savedObjectId !== originalSavedObjectId
+ ) {
+ chrome.recentlyAccessed.add(
+ getFullPath(newInput.savedObjectId),
+ docToSave.title,
+ newInput.savedObjectId
+ );
+
+ // remove editor state so the connection is still broken after reload
+ stateTransfer.clearEditorState?.(APP_ID);
+
+ redirectTo?.(newInput.savedObjectId);
+ return { isLinkedToOriginatingApp: false };
+ }
+
+ const newDoc = {
+ ...docToSave,
+ ...newInput,
+ };
+
+ return { persistedDoc: newDoc, lastKnownDoc: newDoc, isLinkedToOriginatingApp: false };
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.dir(e);
+ trackUiEvent('save_failed');
+ throw e;
+ }
+};
+
+export function getLastKnownDocWithoutPinnedFilters(doc?: Document) {
+ if (!doc) return undefined;
+ const [pinnedFilters, appFilters] = partition(
+ injectFilterReferences(doc.state?.filters || [], doc.references),
+ esFilters.isFilterPinned
+ );
+ return pinnedFilters?.length
+ ? {
+ ...doc,
+ state: {
+ ...doc.state,
+ filters: appFilters,
+ },
+ }
+ : doc;
+}
+
+export const getLastKnownDoc = async ({
+ initialInput,
+ attributeService,
+ data,
+ notifications,
+ chrome,
+}: {
+ initialInput: LensEmbeddableInput;
+ attributeService: LensAttributeService;
+ data: DataPublicPluginStart;
+ notifications: NotificationsStart;
+ chrome: ChromeStart;
+}): Promise<{ doc: Document; indexPatterns: IndexPattern[] } | undefined> => {
+ let doc: Document;
+
+ try {
+ const attributes = await attributeService.unwrapAttributes(initialInput);
+
+ doc = {
+ ...initialInput,
+ ...attributes,
+ type: LENS_EMBEDDABLE_TYPE,
+ };
+
+ if (attributeService.inputIsRefType(initialInput)) {
+ chrome.recentlyAccessed.add(
+ getFullPath(initialInput.savedObjectId),
+ attributes.title,
+ initialInput.savedObjectId
+ );
+ }
+ const indexPatternIds = uniq(
+ doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
+ );
+ const { indexPatterns } = await getAllIndexPatterns(indexPatternIds, data.indexPatterns);
+
+ // Don't overwrite any pinned filters
+ data.query.filterManager.setAppFilters(
+ injectFilterReferences(doc.state.filters, doc.references)
+ );
+ return {
+ doc,
+ indexPatterns,
+ };
+ } catch (e) {
+ notifications.toasts.addDanger(
+ i18n.translate('xpack.lens.app.docLoadingError', {
+ defaultMessage: 'Error loading saved document',
+ })
+ );
+ }
+};
+
+// eslint-disable-next-line import/no-default-export
+export default SaveModalContainer;
diff --git a/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx b/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx
new file mode 100644
index 0000000000000..f1a537fe65928
--- /dev/null
+++ b/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { Suspense, useEffect, useState } from 'react';
+
+import { EuiLoadingSpinner, EuiOverlayMask } from '@elastic/eui';
+import { CoreStart } from 'kibana/public';
+import type { SaveModalContainerProps } from '../save_modal_container';
+import type { LensAttributeService } from '../../lens_attribute_service';
+import type { LensPluginStartDependencies } from '../../plugin';
+import type { LensAppServices } from '../types';
+const SaveModal = React.lazy(() => import('../save_modal_container'));
+
+function LoadingSpinnerWithOverlay() {
+ return (
+
+
+
+ );
+}
+
+const LensSavedModalLazy = (props: SaveModalContainerProps) => {
+ return (
+ }>
+
+
+ );
+};
+
+export function getSaveModalComponent(
+ coreStart: CoreStart,
+ startDependencies: LensPluginStartDependencies,
+ attributeService: () => Promise
+) {
+ return (props: Omit) => {
+ const [lensServices, setLensServices] = useState();
+
+ useEffect(() => {
+ async function loadLensService() {
+ const { getLensServices } = await import('../../async_services');
+ const lensServicesT = await getLensServices(coreStart, startDependencies, attributeService);
+
+ setLensServices(lensServicesT);
+ }
+ loadLensService();
+ }, []);
+
+ if (!lensServices) {
+ return ;
+ }
+
+ const { ContextProvider: PresentationUtilContext } = lensServices.presentationUtil;
+
+ return (
+
+
+
+
+
+ );
+ };
+}
diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts
index 72850552723f3..b253e76aa1407 100644
--- a/x-pack/plugins/lens/public/app_plugin/types.ts
+++ b/x-pack/plugins/lens/public/app_plugin/types.ts
@@ -18,6 +18,8 @@ import {
SavedObjectsStart,
} from '../../../../../src/core/public';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
+import { UsageCollectionStart } from '../../../../../src/plugins/usage_collection/public';
+import { DashboardStart } from '../../../../../src/plugins/dashboard/public';
import { LensEmbeddableInput } from '../editor_frame_service/embeddable/embeddable';
import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public';
import { LensAttributeService } from '../lens_attribute_service';
@@ -33,6 +35,7 @@ import {
EmbeddableStateTransfer,
} from '../../../../../src/plugins/embeddable/public';
import { EditorFrameInstance } from '../types';
+import { PresentationUtilPluginStart } from '../../../../../src/plugins/presentation_util/public';
export interface RedirectToOriginProps {
input?: LensEmbeddableInput;
isCopied?: boolean;
@@ -45,7 +48,6 @@ export interface LensAppProps {
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
redirectTo: (savedObjectId?: string) => void;
redirectToOrigin?: (props?: RedirectToOriginProps) => void;
- redirectToDashboard?: (input: LensEmbeddableInput, dashboardId: string) => void;
// The initial input passed in by the container when editing. Can be either by reference or by value.
initialInput?: LensEmbeddableInput;
@@ -91,16 +93,19 @@ export interface LensAppServices {
chrome: ChromeStart;
overlays: OverlayStart;
storage: IStorageWrapper;
+ dashboard: DashboardStart;
data: DataPublicPluginStart;
uiSettings: IUiSettingsClient;
application: ApplicationStart;
notifications: NotificationsStart;
+ usageCollection?: UsageCollectionStart;
stateTransfer: EmbeddableStateTransfer;
navigation: NavigationPublicPluginStart;
attributeService: LensAttributeService;
savedObjectsClient: SavedObjectsStart['client'];
savedObjectsTagging?: SavedObjectTaggingPluginStart;
getOriginatingAppName: () => string | undefined;
+ presentationUtil: PresentationUtilPluginStart;
// Temporarily required until the 'by value' paradigm is default.
dashboardFeatureFlag: DashboardFeatureFlagConfig;
diff --git a/x-pack/plugins/lens/public/async_services.ts b/x-pack/plugins/lens/public/async_services.ts
index b0ecc412c357f..e7be295955615 100644
--- a/x-pack/plugins/lens/public/async_services.ts
+++ b/x-pack/plugins/lens/public/async_services.ts
@@ -27,3 +27,4 @@ export * from './editor_frame_service/embeddable';
export * from './app_plugin/mounter';
export * from './lens_attribute_service';
export * from './lens_ui_telemetry';
+export * from './app_plugin/save_modal_container';
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap
index a4be46f61990b..7e3c8c3342e4c 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap
@@ -126,7 +126,11 @@ exports[`DatatableComponent it renders actions column when there are row actions
},
},
"cellActions": undefined,
- "display": "a",
+ "display":
+ a
+
,
"displayAsText": "a",
"id": "a",
},
@@ -163,7 +167,11 @@ exports[`DatatableComponent it renders actions column when there are row actions
},
},
"cellActions": undefined,
- "display": "b",
+ "display":
+ b
+
,
"displayAsText": "b",
"id": "b",
},
@@ -200,7 +208,11 @@ exports[`DatatableComponent it renders actions column when there are row actions
},
},
"cellActions": undefined,
- "display": "c",
+ "display":
+ c
+
,
"displayAsText": "c",
"id": "c",
},
@@ -360,7 +372,11 @@ exports[`DatatableComponent it renders the title and value 1`] = `
},
},
"cellActions": undefined,
- "display": "a",
+ "display":
+ a
+
,
"displayAsText": "a",
"id": "a",
},
@@ -397,7 +413,11 @@ exports[`DatatableComponent it renders the title and value 1`] = `
},
},
"cellActions": undefined,
- "display": "b",
+ "display":
+ b
+
,
"displayAsText": "b",
"id": "b",
},
@@ -434,7 +454,11 @@ exports[`DatatableComponent it renders the title and value 1`] = `
},
},
"cellActions": undefined,
- "display": "c",
+ "display":
+ c
+
,
"displayAsText": "c",
"id": "c",
},
@@ -463,7 +487,7 @@ exports[`DatatableComponent it renders the title and value 1`] = `
`;
-exports[`DatatableComponent it should not render actions on header when it is in read only mode 1`] = `
+exports[`DatatableComponent it should render hide and reset actions on header even when it is in read only mode 1`] = `
+ a
+ ,
"displayAsText": "a",
"id": "a",
},
Object {
"actions": Object {
- "additional": Array [],
+ "additional": Array [
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableResetWidth",
+ "iconType": "empty",
+ "isDisabled": true,
+ "label": "Reset width",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ ],
"showHide": false,
"showMoveLeft": false,
"showMoveRight": false,
@@ -580,13 +646,36 @@ exports[`DatatableComponent it should not render actions on header when it is in
"showSortDesc": false,
},
"cellActions": undefined,
- "display": "b",
+ "display":
+ b
+
,
"displayAsText": "b",
"id": "b",
},
Object {
"actions": Object {
- "additional": Array [],
+ "additional": Array [
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableResetWidth",
+ "iconType": "empty",
+ "isDisabled": true,
+ "label": "Reset width",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ ],
"showHide": false,
"showMoveLeft": false,
"showMoveRight": false,
@@ -594,7 +683,11 @@ exports[`DatatableComponent it should not render actions on header when it is in
"showSortDesc": false,
},
"cellActions": undefined,
- "display": "c",
+ "display":
+ c
+
,
"displayAsText": "c",
"id": "c",
},
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
index 5c53d40f999b7..4372e2cd9e964 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
@@ -35,7 +35,8 @@ export const createGridColumns = (
visibleColumns: string[],
formatFactory: FormatFactory,
onColumnResize: (eventData: { columnId: string; width: number | undefined }) => void,
- onColumnHide: (eventData: { columnId: string }) => void
+ onColumnHide: (eventData: { columnId: string }) => void,
+ alignments: Record
) => {
const columnsReverseLookup = table.columns.reduce<
Record
@@ -151,31 +152,33 @@ export const createGridColumns = (
const additionalActions: EuiListGroupItemProps[] = [];
- if (!isReadOnly) {
+ additionalActions.push({
+ color: 'text',
+ size: 'xs',
+ onClick: () => onColumnResize({ columnId: originalColumnId || field, width: undefined }),
+ iconType: 'empty',
+ label: i18n.translate('xpack.lens.table.resize.reset', {
+ defaultMessage: 'Reset width',
+ }),
+ 'data-test-subj': 'lensDatatableResetWidth',
+ isDisabled: initialWidth == null,
+ });
+ if (!isTransposed) {
additionalActions.push({
color: 'text',
size: 'xs',
- onClick: () => onColumnResize({ columnId: originalColumnId || field, width: undefined }),
- iconType: 'empty',
- label: i18n.translate('xpack.lens.table.resize.reset', {
- defaultMessage: 'Reset width',
+ onClick: () => onColumnHide({ columnId: originalColumnId || field }),
+ iconType: 'eyeClosed',
+ label: i18n.translate('xpack.lens.table.hide.hideLabel', {
+ defaultMessage: 'Hide',
}),
- 'data-test-subj': 'lensDatatableResetWidth',
- isDisabled: initialWidth == null,
+ 'data-test-subj': 'lensDatatableHide',
+ isDisabled: !isHidden && visibleColumns.length <= 1,
});
- if (!isTransposed) {
- additionalActions.push({
- color: 'text',
- size: 'xs',
- onClick: () => onColumnHide({ columnId: originalColumnId || field }),
- iconType: 'eyeClosed',
- label: i18n.translate('xpack.lens.table.hide.hideLabel', {
- defaultMessage: 'Hide',
- }),
- 'data-test-subj': 'lensDatatableHide',
- isDisabled: !isHidden && visibleColumns.length <= 1,
- });
- } else if (columnArgs?.bucketValues) {
+ }
+
+ if (!isReadOnly) {
+ if (isTransposed && columnArgs?.bucketValues) {
const bucketValues = columnArgs?.bucketValues;
additionalActions.push({
color: 'text',
@@ -200,11 +203,13 @@ export const createGridColumns = (
});
}
}
+ const currentAlignment = alignments && alignments[field];
+ const alignmentClassName = `lnsTableCell--${currentAlignment}`;
const columnDefinition: EuiDataGridColumn = {
id: field,
cellActions,
- display: name,
+ display: {name}
,
displayAsText: name,
actions: {
showHide: false,
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
index 49003af28f3f1..3479a9e964d53 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { EuiButtonGroup, EuiComboBox, EuiFieldText } from '@elastic/eui';
-import { FramePublicAPI, VisualizationDimensionEditorProps } from '../../types';
+import { FramePublicAPI, Operation, VisualizationDimensionEditorProps } from '../../types';
import { DatatableVisualizationState } from '../visualization';
import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks';
import { mountWithIntl } from '@kbn/test/jest';
@@ -213,6 +213,22 @@ describe('data table dimension editor', () => {
expect(instance.find(PalettePanelContainer).exists()).toBe(true);
});
+ it('should not show the dynamic coloring option for a bucketed operation', () => {
+ frame.activeData!.first.columns[0].meta.type = 'number';
+ frame.datasourceLayers.first.getOperationForColumnId = jest.fn(
+ () => ({ isBucketed: true } as Operation)
+ );
+ state.columns[0].colorMode = 'cell';
+ const instance = mountWithIntl( );
+
+ expect(instance.find('[data-test-subj="lnsDatatable_dynamicColoring_groups"]').exists()).toBe(
+ false
+ );
+ expect(instance.find('[data-test-subj="lnsDatatable_dynamicColoring_palette"]').exists()).toBe(
+ false
+ );
+ });
+
it('should show the summary field for non numeric columns', () => {
const instance = mountWithIntl( );
expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_function"]').exists()).toBe(
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
index 6c39a04ae1504..cf15df07ec72c 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
@@ -104,6 +104,11 @@ export function TableDimensionEditor(
currentData
);
+ const datasource = frame.datasourceLayers[state.layerId];
+ const showDynamicColoringFeature = Boolean(
+ isNumeric && !datasource?.getOperationForColumnId(accessor)?.isBucketed
+ );
+
const visibleColumnsCount = state.columns.filter((c) => !c.hidden).length;
const hasTransposedColumn = state.columns.some(({ isTransposed }) => isTransposed);
@@ -260,7 +265,7 @@ export function TableDimensionEditor(
)}
>
)}
- {isNumeric && (
+ {showDynamicColoringFeature && (
<>
{
).toMatchSnapshot();
});
- test('it should not render actions on header when it is in read only mode', () => {
+ test('it should render hide and reset actions on header even when it is in read only mode', () => {
const { data, args } = sampleArgs();
expect(
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
index cd990149fdaf5..b48cb94563d3b 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
@@ -180,34 +180,6 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
[onEditAction, setColumnConfig, columnConfig]
);
- const columns: EuiDataGridColumn[] = useMemo(
- () =>
- createGridColumns(
- bucketColumns,
- firstLocalTable,
- handleFilterClick,
- handleTransposedColumnClick,
- isReadOnlySorted,
- columnConfig,
- visibleColumns,
- formatFactory,
- onColumnResize,
- onColumnHide
- ),
- [
- bucketColumns,
- firstLocalTable,
- handleFilterClick,
- handleTransposedColumnClick,
- isReadOnlySorted,
- columnConfig,
- visibleColumns,
- formatFactory,
- onColumnResize,
- onColumnHide,
- ]
- );
-
const isNumericMap: Record = useMemo(() => {
const numericMap: Record = {};
for (const column of firstLocalTable.columns) {
@@ -237,6 +209,36 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
);
}, [firstTable, isNumericMap, columnConfig]);
+ const columns: EuiDataGridColumn[] = useMemo(
+ () =>
+ createGridColumns(
+ bucketColumns,
+ firstLocalTable,
+ handleFilterClick,
+ handleTransposedColumnClick,
+ isReadOnlySorted,
+ columnConfig,
+ visibleColumns,
+ formatFactory,
+ onColumnResize,
+ onColumnHide,
+ alignments
+ ),
+ [
+ bucketColumns,
+ firstLocalTable,
+ handleFilterClick,
+ handleTransposedColumnClick,
+ isReadOnlySorted,
+ columnConfig,
+ visibleColumns,
+ formatFactory,
+ onColumnResize,
+ onColumnHide,
+ alignments,
+ ]
+ );
+
const trailingControlColumns: EuiDataGridControlColumn[] = useMemo(() => {
if (!hasAtLeastOneRowClickAction || !onRowContextMenuClick) {
return [];
@@ -278,9 +280,13 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
[formatters, columnConfig, props.uiSettings]
);
- const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns: () => {} }), [
- visibleColumns,
- ]);
+ const columnVisibility = useMemo(
+ () => ({
+ visibleColumns,
+ setVisibleColumns: () => {},
+ }),
+ [visibleColumns]
+ );
const sorting = useMemo(
() => createGridSortingConfig(sortBy, sortDirection as LensGridDirection, onEditAction),
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
index ba0e09bdd894c..0c3a992e3dd7a 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
@@ -297,7 +297,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
),
append:
- v.selection.dataLoss !== 'nothing' || v.showBetaBadge ? (
+ v.selection.dataLoss !== 'nothing' || v.showExperimentalBadge ? (
) : null}
- {v.showBetaBadge ? (
+ {v.showExperimentalBadge ? (
diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx
index 54f9c70824831..fce5bf30f47ed 100644
--- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx
@@ -88,7 +88,7 @@ export const getHeatmapVisualization = ({
defaultMessage: 'Heatmap',
}),
groupLabel: groupLabelForBar,
- showBetaBadge: true,
+ showExperimentalBadge: true,
},
],
diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts
index 0fdd3bf426232..98e0198b9d0fa 100644
--- a/x-pack/plugins/lens/public/index.ts
+++ b/x-pack/plugins/lens/public/index.ts
@@ -55,6 +55,8 @@ export type {
DerivativeIndexPatternColumn,
MovingAverageIndexPatternColumn,
} from './indexpattern_datasource/types';
+export type { LensEmbeddableInput } from './editor_frame_service/embeddable';
+
export { LensPublicStart } from './plugin';
export const plugin = () => new LensPlugin();
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
index 3dd2d4a4ba3f5..4ffd0db52d374 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
@@ -146,6 +146,11 @@ export function DimensionEditor(props: DimensionEditorProps) {
const possibleOperations = useMemo(() => {
return Object.values(operationDefinitionMap)
.filter(({ hidden }) => !hidden)
+ .filter(
+ (operationDefinition) =>
+ !('selectionStyle' in operationDefinition) ||
+ operationDefinition.selectionStyle !== 'hidden'
+ )
.filter(({ type }) => fieldByOperation[type]?.size || operationWithoutField.has(type))
.sort((op1, op2) => {
return op1.displayName.localeCompare(op2.displayName);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/index.ts
index 815acb8c4169f..a7741bc60d646 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/index.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/index.ts
@@ -9,3 +9,13 @@ export { counterRateOperation, CounterRateIndexPatternColumn } from './counter_r
export { cumulativeSumOperation, CumulativeSumIndexPatternColumn } from './cumulative_sum';
export { derivativeOperation, DerivativeIndexPatternColumn } from './differences';
export { movingAverageOperation, MovingAverageIndexPatternColumn } from './moving_average';
+export {
+ overallSumOperation,
+ OverallSumIndexPatternColumn,
+ overallMinOperation,
+ OverallMinIndexPatternColumn,
+ overallMaxOperation,
+ OverallMaxIndexPatternColumn,
+ overallAverageOperation,
+ OverallAverageIndexPatternColumn,
+} from './overall_metric';
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/overall_metric.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/overall_metric.tsx
new file mode 100644
index 0000000000000..21ec5387b3853
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/overall_metric.tsx
@@ -0,0 +1,224 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types';
+import { optionallHistogramBasedOperationToExpression } from './utils';
+import { OperationDefinition } from '..';
+import { getFormatFromPreviousColumn } from '../helpers';
+
+type OverallMetricIndexPatternColumn = FormattedIndexPatternColumn &
+ ReferenceBasedIndexPatternColumn & {
+ operationType: T;
+ };
+
+export type OverallSumIndexPatternColumn = OverallMetricIndexPatternColumn<'overall_sum'>;
+export type OverallMinIndexPatternColumn = OverallMetricIndexPatternColumn<'overall_min'>;
+export type OverallMaxIndexPatternColumn = OverallMetricIndexPatternColumn<'overall_max'>;
+export type OverallAverageIndexPatternColumn = OverallMetricIndexPatternColumn<'overall_average'>;
+
+function buildOverallMetricOperation>({
+ type,
+ displayName,
+ ofName,
+ description,
+ metric,
+}: {
+ type: T['operationType'];
+ displayName: string;
+ ofName: (name?: string) => string;
+ description: string;
+ metric: string;
+}): OperationDefinition {
+ return {
+ type,
+ priority: 1,
+ displayName,
+ input: 'fullReference',
+ selectionStyle: 'hidden',
+ requiredReferences: [
+ {
+ input: ['field', 'managedReference', 'fullReference'],
+ validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed,
+ },
+ ],
+ getPossibleOperation: () => {
+ return {
+ dataType: 'number',
+ isBucketed: false,
+ scale: 'ratio',
+ };
+ },
+ getDefaultLabel: (column, indexPattern, columns) => {
+ const ref = columns[column.references[0]];
+ return ofName(
+ ref && 'sourceField' in ref
+ ? indexPattern.getFieldByName(ref.sourceField)?.displayName
+ : undefined
+ );
+ },
+ toExpression: (layer, columnId) => {
+ return optionallHistogramBasedOperationToExpression(layer, columnId, 'overall_metric', {
+ metric: [metric],
+ });
+ },
+ buildColumn: ({ referenceIds, previousColumn, layer, indexPattern }, columnParams) => {
+ const ref = layer.columns[referenceIds[0]];
+ return {
+ label: ofName(
+ ref && 'sourceField' in ref
+ ? indexPattern.getFieldByName(ref.sourceField)?.displayName
+ : undefined
+ ),
+ dataType: 'number',
+ operationType: 'overall_sum',
+ isBucketed: false,
+ scale: 'ratio',
+ references: referenceIds,
+ params: getFormatFromPreviousColumn(previousColumn),
+ } as T;
+ },
+ isTransferable: () => {
+ return true;
+ },
+ filterable: false,
+ shiftable: false,
+ documentation: {
+ section: 'calculation',
+ signature: i18n.translate('xpack.lens.indexPattern.overall_metric', {
+ defaultMessage: 'metric: number',
+ }),
+ description,
+ },
+ };
+}
+
+export const overallSumOperation = buildOverallMetricOperation({
+ type: 'overall_sum',
+ displayName: i18n.translate('xpack.lens.indexPattern.overallSum', {
+ defaultMessage: 'Overall sum',
+ }),
+ ofName: (name?: string) => {
+ return i18n.translate('xpack.lens.indexPattern.overallSumOf', {
+ defaultMessage: 'Overall sum of {name}',
+ values: {
+ name:
+ name ??
+ i18n.translate('xpack.lens.indexPattern.incompleteOperation', {
+ defaultMessage: '(incomplete)',
+ }),
+ },
+ });
+ },
+ metric: 'sum',
+ description: i18n.translate('xpack.lens.indexPattern.overall_sum.documentation', {
+ defaultMessage: `
+Calculates the sum of a metric of all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function.
+Other dimensions breaking down the data like top values or filter are treated as separate series.
+
+If no date histograms or interval functions are used in the current chart, \`overall_sum\` is calculating the sum over all dimensions no matter the used function.
+
+Example: Percentage of total
+\`sum(bytes) / overall_sum(sum(bytes))\`
+ `,
+ }),
+});
+
+export const overallMinOperation = buildOverallMetricOperation({
+ type: 'overall_min',
+ displayName: i18n.translate('xpack.lens.indexPattern.overallMin', {
+ defaultMessage: 'Overall min',
+ }),
+ ofName: (name?: string) => {
+ return i18n.translate('xpack.lens.indexPattern.overallMinOf', {
+ defaultMessage: 'Overall min of {name}',
+ values: {
+ name:
+ name ??
+ i18n.translate('xpack.lens.indexPattern.incompleteOperation', {
+ defaultMessage: '(incomplete)',
+ }),
+ },
+ });
+ },
+ metric: 'min',
+ description: i18n.translate('xpack.lens.indexPattern.overall_min.documentation', {
+ defaultMessage: `
+Calculates the minimum of a metric for all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function.
+Other dimensions breaking down the data like top values or filter are treated as separate series.
+
+If no date histograms or interval functions are used in the current chart, \`overall_min\` is calculating the minimum over all dimensions no matter the used function
+
+Example: Percentage of range
+\`(sum(bytes) - overall_min(sum(bytes)) / (overall_max(bytes) - overall_min(bytes))\`
+ `,
+ }),
+});
+
+export const overallMaxOperation = buildOverallMetricOperation({
+ type: 'overall_max',
+ displayName: i18n.translate('xpack.lens.indexPattern.overallMax', {
+ defaultMessage: 'Overall max',
+ }),
+ ofName: (name?: string) => {
+ return i18n.translate('xpack.lens.indexPattern.overallMaxOf', {
+ defaultMessage: 'Overall max of {name}',
+ values: {
+ name:
+ name ??
+ i18n.translate('xpack.lens.indexPattern.incompleteOperation', {
+ defaultMessage: '(incomplete)',
+ }),
+ },
+ });
+ },
+ metric: 'max',
+ description: i18n.translate('xpack.lens.indexPattern.overall_max.documentation', {
+ defaultMessage: `
+Calculates the maximum of a metric for all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function.
+Other dimensions breaking down the data like top values or filter are treated as separate series.
+
+If no date histograms or interval functions are used in the current chart, \`overall_max\` is calculating the maximum over all dimensions no matter the used function
+
+Example: Percentage of range
+\`(sum(bytes) - overall_min(sum(bytes)) / (overall_max(bytes) - overall_min(bytes))\`
+ `,
+ }),
+});
+
+export const overallAverageOperation = buildOverallMetricOperation(
+ {
+ type: 'overall_average',
+ displayName: i18n.translate('xpack.lens.indexPattern.overallMax', {
+ defaultMessage: 'Overall max',
+ }),
+ ofName: (name?: string) => {
+ return i18n.translate('xpack.lens.indexPattern.overallAverageOf', {
+ defaultMessage: 'Overall average of {name}',
+ values: {
+ name:
+ name ??
+ i18n.translate('xpack.lens.indexPattern.incompleteOperation', {
+ defaultMessage: '(incomplete)',
+ }),
+ },
+ });
+ },
+ metric: 'average',
+ description: i18n.translate('xpack.lens.indexPattern.overall_average.documentation', {
+ defaultMessage: `
+Calculates the average of a metric for all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function.
+Other dimensions breaking down the data like top values or filter are treated as separate series.
+
+If no date histograms or interval functions are used in the current chart, \`overall_average\` is calculating the average over all dimensions no matter the used function
+
+Example: Divergence from the mean:
+\`sum(bytes) - overall_average(sum(bytes))\`
+ `,
+ }),
+ }
+);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
index 1f4f097c6a7fb..03b9d6c07709c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
@@ -134,3 +134,35 @@ export function dateBasedOperationToExpression(
},
];
}
+
+/**
+ * Creates an expression ast for a date based operation (cumulative sum, derivative, moving average, counter rate)
+ */
+export function optionallHistogramBasedOperationToExpression(
+ layer: IndexPatternLayer,
+ columnId: string,
+ functionName: string,
+ additionalArgs: Record = {}
+): ExpressionFunctionAST[] {
+ const currentColumn = (layer.columns[columnId] as unknown) as ReferenceBasedIndexPatternColumn;
+ const buckets = layer.columnOrder.filter((colId) => layer.columns[colId].isBucketed);
+ const nonHistogramColumns = buckets.filter(
+ (colId) =>
+ layer.columns[colId].operationType !== 'date_histogram' &&
+ layer.columns[colId].operationType !== 'range'
+ )!;
+
+ return [
+ {
+ type: 'function',
+ function: functionName,
+ arguments: {
+ by: nonHistogramColumns.length === buckets.length ? [] : nonHistogramColumns,
+ inputColumnId: [currentColumn.references[0]],
+ outputColumnId: [columnId],
+ outputColumnName: [currentColumn.label],
+ ...additionalArgs,
+ },
+ },
+ ];
+}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
index a7bf415817797..c38475f85f47e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
@@ -33,6 +33,14 @@ import {
DerivativeIndexPatternColumn,
movingAverageOperation,
MovingAverageIndexPatternColumn,
+ OverallSumIndexPatternColumn,
+ overallSumOperation,
+ OverallMinIndexPatternColumn,
+ overallMinOperation,
+ OverallMaxIndexPatternColumn,
+ overallMaxOperation,
+ OverallAverageIndexPatternColumn,
+ overallAverageOperation,
} from './calculations';
import { countOperation, CountIndexPatternColumn } from './count';
import {
@@ -71,6 +79,10 @@ export type IndexPatternColumn =
| CountIndexPatternColumn
| LastValueIndexPatternColumn
| CumulativeSumIndexPatternColumn
+ | OverallSumIndexPatternColumn
+ | OverallMinIndexPatternColumn
+ | OverallMaxIndexPatternColumn
+ | OverallAverageIndexPatternColumn
| CounterRateIndexPatternColumn
| DerivativeIndexPatternColumn
| MovingAverageIndexPatternColumn
@@ -98,6 +110,10 @@ export {
CounterRateIndexPatternColumn,
DerivativeIndexPatternColumn,
MovingAverageIndexPatternColumn,
+ OverallSumIndexPatternColumn,
+ OverallMinIndexPatternColumn,
+ OverallMaxIndexPatternColumn,
+ OverallAverageIndexPatternColumn,
} from './calculations';
export { CountIndexPatternColumn } from './count';
export { LastValueIndexPatternColumn } from './last_value';
@@ -126,6 +142,10 @@ const internalOperationDefinitions = [
movingAverageOperation,
mathOperation,
formulaOperation,
+ overallSumOperation,
+ overallMinOperation,
+ overallMaxOperation,
+ overallAverageOperation,
];
export { termsOperation } from './terms';
@@ -141,6 +161,10 @@ export {
counterRateOperation,
derivativeOperation,
movingAverageOperation,
+ overallSumOperation,
+ overallAverageOperation,
+ overallMaxOperation,
+ overallMinOperation,
} from './calculations';
export { formulaOperation } from './formula/formula';
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts
index aa46dd765bd8b..d55c5d3c00f17 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts
@@ -31,4 +31,8 @@ export {
CounterRateIndexPatternColumn,
DerivativeIndexPatternColumn,
MovingAverageIndexPatternColumn,
+ OverallSumIndexPatternColumn,
+ OverallMinIndexPatternColumn,
+ OverallMaxIndexPatternColumn,
+ OverallAverageIndexPatternColumn,
} from './definitions';
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
index ba3bee415f3f4..1ae2f4421a0bc 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
@@ -1797,12 +1797,14 @@ describe('state_helpers', () => {
it('should promote the inner references when switching away from reference to field-based operation (case a2)', () => {
const expectedCol = {
- label: 'Count of records',
+ label: 'Count of records -3h',
dataType: 'number' as const,
isBucketed: false,
operationType: 'count' as const,
sourceField: 'Records',
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
};
const layer: IndexPatternLayer = {
indexPatternId: '1',
@@ -1817,6 +1819,8 @@ describe('state_helpers', () => {
// @ts-expect-error not a valid type
operationType: 'testReference',
references: ['col1'],
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
},
},
};
@@ -1845,6 +1849,8 @@ describe('state_helpers', () => {
isBucketed: false,
sourceField: 'bytes',
operationType: 'average' as const,
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
};
const layer: IndexPatternLayer = {
@@ -1858,6 +1864,8 @@ describe('state_helpers', () => {
isBucketed: false,
operationType: 'differences',
references: ['metric'],
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
},
},
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
index b650a2818b2d4..4e3bcec4b6ca2 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
@@ -414,11 +414,21 @@ export function replaceColumn({
indexPattern,
});
+ const column = copyCustomLabel({ ...referenceColumn }, previousColumn);
+ // do not forget to move over also any filter/shift/anything (if compatible)
+ // from the reference definition to the new operation.
+ if (referencedOperation.filterable) {
+ column.filter = (previousColumn as ReferenceBasedIndexPatternColumn).filter;
+ }
+ if (referencedOperation.shiftable) {
+ column.timeShift = (previousColumn as ReferenceBasedIndexPatternColumn).timeShift;
+ }
+
tempLayer = {
...tempLayer,
columns: {
...tempLayer.columns,
- [columnId]: copyCustomLabel({ ...referenceColumn }, previousColumn),
+ [columnId]: column,
},
};
return updateDefaultLabels(
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts
index 7df096c27d9a0..2ed6e2b3a7bcb 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts
@@ -319,6 +319,22 @@ describe('getOperationTypesForField', () => {
"operationType": "moving_average",
"type": "fullReference",
},
+ Object {
+ "operationType": "overall_sum",
+ "type": "fullReference",
+ },
+ Object {
+ "operationType": "overall_min",
+ "type": "fullReference",
+ },
+ Object {
+ "operationType": "overall_max",
+ "type": "fullReference",
+ },
+ Object {
+ "operationType": "overall_average",
+ "type": "fullReference",
+ },
Object {
"field": "bytes",
"operationType": "min",
diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx
index 07935bb2f241b..b35353b98a585 100644
--- a/x-pack/plugins/lens/public/mocks.tsx
+++ b/x-pack/plugins/lens/public/mocks.tsx
@@ -21,6 +21,7 @@ import { navigationPluginMock } from '../../../../src/plugins/navigation/public/
import { LensAppServices } from './app_plugin/types';
import { DOC_TYPE } from '../common';
import { DataPublicPluginStart, esFilters, UI_SETTINGS } from '../../../../src/plugins/data/public';
+import { dashboardPluginMock } from '../../../../src/plugins/dashboard/public/mocks';
import {
LensByValueInput,
LensSavedObjectAttributes,
@@ -35,6 +36,7 @@ import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/publ
import { makeConfigureStore, getPreloadedState, LensAppState } from './state_management/index';
import { getResolvedDateRange } from './utils';
+import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks';
export type Start = jest.Mocked;
@@ -43,6 +45,9 @@ const createStartContract = (): Start => {
EmbeddableComponent: jest.fn(() => {
return Lens Embeddable Component ;
}),
+ SaveModalComponent: jest.fn(() => {
+ return Lens Save Modal Component ;
+ }),
canUseEditor: jest.fn(() => true),
navigateToPrefilledEditor: jest.fn(),
getXyVisTypes: jest.fn().mockReturnValue(new Promise((resolve) => resolve(visualizationTypes))),
@@ -228,6 +233,8 @@ export function makeDefaultServices(
navigation: navigationStartMock,
notifications: core.notifications,
attributeService: makeAttributeService(),
+ dashboard: dashboardPluginMock.createStartContract(),
+ presentationUtil: presentationUtilPluginMock.createStartContract(core),
savedObjectsClient: core.savedObjects.client,
dashboardFeatureFlag: { allowByValueEmbeddables: false },
stateTransfer: createEmbeddableStateTransferMock() as EmbeddableStateTransfer,
diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts
index fe225dba6f256..328bea5def557 100644
--- a/x-pack/plugins/lens/public/plugin.ts
+++ b/x-pack/plugins/lens/public/plugin.ts
@@ -6,7 +6,7 @@
*/
import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public';
-import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
+import { UsageCollectionSetup, UsageCollectionStart } from 'src/plugins/usage_collection/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import { DashboardStart } from '../../../../src/plugins/dashboard/public';
@@ -55,6 +55,8 @@ import {
getEmbeddableComponent,
} from './editor_frame_service/embeddable/embeddable_component';
import { HeatmapVisualization } from './heatmap_visualization';
+import { getSaveModalComponent } from './app_plugin/shared/saved_modal_lazy';
+import { SaveModalContainerProps } from './app_plugin/save_modal_container';
export interface LensPluginSetupDependencies {
urlForwarding: UrlForwardingSetup;
@@ -79,6 +81,7 @@ export interface LensPluginStartDependencies {
savedObjectsTagging?: SavedObjectTaggingPluginStart;
presentationUtil: PresentationUtilPluginStart;
indexPatternFieldEditor: IndexPatternFieldEditorStart;
+ usageCollection?: UsageCollectionStart;
}
export interface LensPublicStart {
@@ -91,6 +94,15 @@ export interface LensPublicStart {
* @experimental
*/
EmbeddableComponent: React.ComponentType;
+ /**
+ * React component which can be used to embed a Lens Visualization Save Modal Component.
+ * See `x-pack/examples/embedded_lens_example` for exemplary usage.
+ *
+ * This API might undergo breaking changes even in minor versions.
+ *
+ * @experimental
+ */
+ SaveModalComponent: React.ComponentType>;
/**
* Method which navigates to the Lens editor, loading the state specified by the `input` parameter.
* See `x-pack/examples/embedded_lens_example` for exemplary usage.
@@ -185,11 +197,6 @@ export class LensPlugin {
visualizations.registerAlias(getLensAliasConfig());
- const getByValueFeatureFlag = async () => {
- const [, deps] = await core.getStartServices();
- return deps.dashboard.dashboardFeatureFlagConfig;
- };
-
const getPresentationUtilContext = async () => {
const [, deps] = await core.getStartServices();
const { ContextProvider } = deps.presentationUtil;
@@ -214,7 +221,6 @@ export class LensPlugin {
return mountApp(core, params, {
createEditorFrame: this.createEditorFrame!,
attributeService: this.attributeService!,
- getByValueFeatureFlag,
getPresentationUtilContext,
});
},
@@ -251,6 +257,7 @@ export class LensPlugin {
return {
EmbeddableComponent: getEmbeddableComponent(startDependencies.embeddable),
+ SaveModalComponent: getSaveModalComponent(core, startDependencies, this.attributeService!),
navigateToPrefilledEditor: (input: LensEmbeddableInput, openInNewTab?: boolean) => {
// for openInNewTab, we set the time range in url via getEditPath below
if (input.timeRange && !openInNewTab) {
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index b421d57dae6e1..7baba15f0fac6 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -49,6 +49,7 @@ export interface EditorFrameProps {
initialContext?: VisualizeFieldContext;
showNoDataPopover: () => void;
}
+
export interface EditorFrameInstance {
EditorFrameContainer: (props: EditorFrameProps) => React.ReactElement;
}
@@ -570,9 +571,9 @@ export interface VisualizationType {
*/
sortPriority?: number;
/**
- * Indicates if visualization is in the beta stage.
+ * Indicates if visualization is in the experimental stage.
*/
- showBetaBadge?: boolean;
+ showExperimentalBadge?: boolean;
}
export interface Visualization {
@@ -734,6 +735,7 @@ interface LensEditContextMapping {
[LENS_EDIT_RESIZE_ACTION]: LensResizeActionData;
[LENS_TOGGLE_ACTION]: LensToggleActionData;
}
+
type LensEditSupportedActions = keyof LensEditContextMapping;
export type LensEditPayload = {
@@ -746,6 +748,7 @@ export interface LensEditEvent {
name: 'edit';
data: EditPayloadContext;
}
+
export interface LensTableRowContextMenuEvent {
name: 'tableRowContextMenuClick';
data: RowClickContext['data'];
diff --git a/x-pack/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts
index b9a526c71180c..5b48ef8b31923 100644
--- a/x-pack/plugins/lens/public/vis_type_alias.ts
+++ b/x-pack/plugins/lens/public/vis_type_alias.ts
@@ -42,6 +42,7 @@ export const getLensAliasConfig = (): VisTypeAlias => ({
icon: 'lensApp',
stage: 'production',
savedObjectType: type,
+ type: 'lens',
typeTitle: i18n.translate('xpack.lens.visTypeAlias.type', { defaultMessage: 'Lens' }),
};
},
diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx
index 047c95846cd27..2e5fdf8493e24 100644
--- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx
@@ -33,6 +33,7 @@ describe('Axes Settings', () => {
toggleTickLabelsVisibility: jest.fn(),
toggleGridlinesVisibility: jest.fn(),
hasBarOrAreaOnAxis: false,
+ hasPercentageAxis: false,
};
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx
index 43ebc91f533a4..a0d1dae2145d5 100644
--- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx
@@ -31,6 +31,7 @@ import { ToolbarButtonProps } from '../../../../../src/plugins/kibana_react/publ
import { validateExtent } from './axes_configuration';
type AxesSettingsConfigKeys = keyof AxesSettingsConfig;
+
export interface AxisSettingsPopoverProps {
/**
* Determines the axis
@@ -93,8 +94,10 @@ export interface AxisSettingsPopoverProps {
*/
setExtent?: (extent: AxisExtentConfig | undefined) => void;
hasBarOrAreaOnAxis: boolean;
+ hasPercentageAxis: boolean;
dataBounds?: { min: number; max: number };
}
+
const popoverConfig = (
axis: AxesSettingsConfigKeys,
isHorizontal: boolean
@@ -168,6 +171,7 @@ export const AxisSettingsPopover: React.FunctionComponent {
const isHorizontal = layers?.length ? isHorizontalChart(layers) : false;
@@ -333,10 +337,13 @@ export const AxisSettingsPopover: React.FunctionComponent {
const newMode = id.replace(idPrefix, '') as AxisExtentConfig['mode'];
@@ -350,7 +357,7 @@ export const AxisSettingsPopover: React.FunctionComponent
- {localExtent.mode === 'custom' && (
+ {localExtent.mode === 'custom' && !hasPercentageAxis && (
<>
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
index b3d1f8f062b73..04533f6c914e1 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
@@ -36,6 +36,7 @@ import {
YAxisMode,
AxesSettingsConfig,
AxisExtentConfig,
+ XYState,
} from './types';
import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers';
import { trackUiEvent } from '../lens_ui_telemetry';
@@ -162,6 +163,18 @@ const getDataBounds = function (
return groups;
};
+function hasPercentageAxis(axisGroups: GroupsConfiguration, groupId: string, state: XYState) {
+ return Boolean(
+ axisGroups
+ .find((group) => group.groupId === groupId)
+ ?.series.some(({ layer: layerId }) =>
+ state?.layers.find(
+ (layer) => layer.layerId === layerId && layer.seriesType.includes('percentage')
+ )
+ )
+ );
+}
+
export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProps) {
const { state, setState, frame } = props;
@@ -377,6 +390,7 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp
setExtent={setLeftExtent}
hasBarOrAreaOnAxis={hasBarOrAreaOnLeftAxis}
dataBounds={dataBounds.left}
+ hasPercentageAxis={hasPercentageAxis(axisGroups, 'left', state)}
/>
group.groupId === 'right') || {}).length ===
0
}
+ hasPercentageAxis={hasPercentageAxis(axisGroups, 'right', state)}
isAxisTitleVisible={axisTitlesVisibilitySettings.yRight}
toggleAxisTitleVisibility={onAxisTitlesVisibilitySettingsChange}
extent={state?.yRightExtent || { mode: 'full' }}
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
index 8033f8d187fd5..edadc20a595ce 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
+++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
@@ -374,11 +374,19 @@ export function clamp(val: number, min: number, max: number): number {
export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent {
const width = bounds.maxLon - bounds.minLon;
const height = bounds.maxLat - bounds.minLat;
+
+ const newMinLon = bounds.minLon - width * scaleFactor;
+ const nexMaxLon = bounds.maxLon + width * scaleFactor;
+
+ const lonDelta = nexMaxLon - newMinLon;
+ const left = lonDelta > 360 ? -180 : newMinLon;
+ const right = lonDelta > 360 ? 180 : nexMaxLon;
+
return {
- minLon: bounds.minLon - width * scaleFactor,
- minLat: bounds.minLat - height * scaleFactor,
- maxLon: bounds.maxLon + width * scaleFactor,
- maxLat: bounds.maxLat + height * scaleFactor,
+ minLon: left,
+ minLat: clampToLatBounds(bounds.minLat - height * scaleFactor),
+ maxLon: right,
+ maxLat: clampToLonBounds(bounds.maxLat + height * scaleFactor),
};
}
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts
index d828aca4a1a00..7aef32dfb4f8a 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts
+++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts
@@ -23,11 +23,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -89,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [-89, 39],
- bottom_right: [-83, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [-89, 39],
+ bottom_right: [-83, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -39,11 +59,31 @@ describe('createExtentFilter', () => {
minLat: -100,
minLon: -190,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [-180, 89],
- bottom_right: [180, -89],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [-180, 89],
+ bottom_right: [180, -89],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -55,11 +95,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: 100,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [100, 39],
- bottom_right: [-160, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [100, 39],
+ bottom_right: [-160, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -71,11 +131,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -200,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [160, 39],
- bottom_right: [-100, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [160, 39],
+ bottom_right: [-100, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -87,11 +167,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -191,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [-180, 39],
- bottom_right: [180, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [-180, 39],
+ bottom_right: [180, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -184,23 +284,36 @@ describe('createSpatialFilterWithGeometry', () => {
negate: false,
type: 'spatial_filter',
},
- geo_shape: {
- 'geo.coordinates': {
- relation: 'INTERSECTS',
- shape: {
- coordinates: [
- [
- [-101.21639, 48.1413],
- [-101.21639, 41.84905],
- [-90.95149, 41.84905],
- [-90.95149, 48.1413],
- [-101.21639, 48.1413],
- ],
- ],
- type: 'Polygon',
- },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'geo.coordinates',
+ },
+ },
+ {
+ geo_shape: {
+ 'geo.coordinates': {
+ relation: 'INTERSECTS',
+ shape: {
+ coordinates: [
+ [
+ [-101.21639, 48.1413],
+ [-101.21639, 41.84905],
+ [-90.95149, 41.84905],
+ [-90.95149, 48.1413],
+ [-101.21639, 48.1413],
+ ],
+ ],
+ type: 'Polygon',
+ },
+ },
+ ignore_unmapped: true,
+ },
+ },
+ ],
},
- ignore_unmapped: true,
},
});
});
@@ -318,9 +431,22 @@ describe('createDistanceFilterWithMeta', () => {
negate: false,
type: 'spatial_filter',
},
- geo_distance: {
- distance: '1000km',
- 'geo.coordinates': [120, 30],
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'geo.coordinates',
+ },
+ },
+ {
+ geo_distance: {
+ distance: '1000km',
+ 'geo.coordinates': [120, 30],
+ },
+ },
+ ],
+ },
},
});
});
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts
index 70df9e9646f50..9a2b2c21136df 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts
+++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts
@@ -31,13 +31,23 @@ function createMultiGeoFieldFilter(
}
if (geoFieldNames.length === 1) {
- const geoFilter = createGeoFilter(geoFieldNames[0]);
return {
meta: {
...meta,
key: geoFieldNames[0],
},
- ...geoFilter,
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: geoFieldNames[0],
+ },
+ },
+ createGeoFilter(geoFieldNames[0]),
+ ],
+ },
+ },
};
}
@@ -201,8 +211,9 @@ export function extractFeaturesFromFilters(filters: GeoFilter[]): Feature[] {
}
} else {
const geoFieldName = filter.meta.key;
- if (geoFieldName) {
- geometry = extractGeometryFromFilter(geoFieldName, filter);
+ const spatialClause = filter?.query?.bool?.must?.[1];
+ if (geoFieldName && spatialClause) {
+ geometry = extractGeometryFromFilter(geoFieldName, spatialClause);
}
}
diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js
index 77ce23594447f..fa69dad616747 100644
--- a/x-pack/plugins/maps/public/actions/map_actions.test.js
+++ b/x-pack/plugins/maps/public/actions/map_actions.test.js
@@ -33,7 +33,9 @@ describe('map_actions', () => {
describe('store mapState is empty', () => {
beforeEach(() => {
require('../selectors/map_selectors').getDataFilters = () => {
- return {};
+ return {
+ zoom: 5,
+ };
};
require('../selectors/map_selectors').getLayerList = () => {
@@ -61,11 +63,13 @@ describe('map_actions', () => {
minLat: 5,
minLon: 95,
},
+ zoom: 5,
});
await action(dispatchMock, getStoreMock);
expect(dispatchMock).toHaveBeenCalledWith({
mapState: {
+ zoom: 5,
extent: {
maxLat: 10,
maxLon: 100,
@@ -73,10 +77,10 @@ describe('map_actions', () => {
minLon: 95,
},
buffer: {
- maxLat: 12.5,
- maxLon: 102.5,
- minLat: 2.5,
- minLon: 92.5,
+ maxLat: 21.94305,
+ maxLon: 112.5,
+ minLat: 0,
+ minLon: 90,
},
},
type: 'MAP_EXTENT_CHANGED',
@@ -154,10 +158,10 @@ describe('map_actions', () => {
minLon: 85,
},
buffer: {
- maxLat: 7.5,
- maxLon: 92.5,
- minLat: -2.5,
- minLon: 82.5,
+ maxLat: 7.71099,
+ maxLon: 92.8125,
+ minLat: -2.81137,
+ minLon: 82.26563,
},
},
type: 'MAP_EXTENT_CHANGED',
@@ -186,10 +190,10 @@ describe('map_actions', () => {
minLon: 96,
},
buffer: {
- maxLat: 13.5,
- maxLon: 103.5,
- minLat: 3.5,
- minLon: 93.5,
+ maxLat: 13.58192,
+ maxLon: 103.53516,
+ minLat: 3.33795,
+ minLon: 93.33984,
},
},
type: 'MAP_EXTENT_CHANGED',
diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts
index 42ce96d102d7e..3cdc5bf05ccee 100644
--- a/x-pack/plugins/maps/public/actions/map_actions.ts
+++ b/x-pack/plugins/maps/public/actions/map_actions.ts
@@ -56,6 +56,7 @@ import {
import { INITIAL_LOCATION } from '../../common/constants';
import { scaleBounds } from '../../common/elasticsearch_util';
import { cleanTooltipStateForLayer } from './tooltip_actions';
+import { expandToTileBoundaries } from '../../common/geo_tile_utils';
export interface MapExtentState {
zoom: number;
@@ -158,7 +159,9 @@ export function mapExtentChanged(mapExtentState: MapExtentState) {
}
if (!doesBufferContainExtent || currentZoom !== newZoom) {
- dataFilters.buffer = scaleBounds(extent, 0.5);
+ const expandedExtent = scaleBounds(extent, 0.5);
+ // snap to the smallest tile-bounds, to avoid jitter in the bounds
+ dataFilters.buffer = expandToTileBoundaries(expandedExtent, Math.ceil(newZoom));
}
}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
index ad413419a289b..1043ed8778304 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
@@ -205,13 +205,31 @@ describe('ESGeoGridSource', () => {
expect(getProperty('query')).toEqual(undefined);
expect(getProperty('filter')).toEqual([
{
- geo_bounding_box: { bar: { bottom_right: [180, -82.67628], top_left: [-180, 82.67628] } },
meta: {
alias: null,
disabled: false,
key: 'bar',
negate: false,
},
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'bar',
+ },
+ },
+ {
+ geo_bounding_box: {
+ bar: {
+ top_left: [-180, 82.67628],
+ bottom_right: [180, -82.67628],
+ },
+ },
+ },
+ ],
+ },
+ },
},
]);
expect(getProperty('aggs')).toEqual({
@@ -262,20 +280,33 @@ describe('ESGeoGridSource', () => {
});
describe('ITiledSingleLayerVectorSource', () => {
+ const mvtGeogridSource = new ESGeoGridSource(
+ {
+ id: 'foobar',
+ indexPatternId: 'fooIp',
+ geoField: geoFieldName,
+ metrics: [],
+ resolution: GRID_RESOLUTION.SUPER_FINE,
+ type: SOURCE_TYPES.ES_GEO_GRID,
+ requestType: RENDER_AS.HEATMAP,
+ },
+ {}
+ );
+
it('getLayerName', () => {
- expect(geogridSource.getLayerName()).toBe('source_layer');
+ expect(mvtGeogridSource.getLayerName()).toBe('source_layer');
});
it('getMinZoom', () => {
- expect(geogridSource.getMinZoom()).toBe(0);
+ expect(mvtGeogridSource.getMinZoom()).toBe(0);
});
it('getMaxZoom', () => {
- expect(geogridSource.getMaxZoom()).toBe(24);
+ expect(mvtGeogridSource.getMaxZoom()).toBe(24);
});
it('getUrlTemplateWithMeta', async () => {
- const urlTemplateWithMeta = await geogridSource.getUrlTemplateWithMeta(
+ const urlTemplateWithMeta = await mvtGeogridSource.getUrlTemplateWithMeta(
vectorSourceRequestMeta
);
@@ -283,19 +314,19 @@ describe('ESGeoGridSource', () => {
expect(urlTemplateWithMeta.minSourceZoom).toBe(0);
expect(urlTemplateWithMeta.maxSourceZoom).toBe(24);
expect(urlTemplateWithMeta.urlTemplate).toEqual(
- "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628))),meta:(alias:!n,disabled:!f,key:bar,negate:!f)))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point"
+ "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point"
);
});
it('should include searchSourceId in urlTemplateWithMeta', async () => {
- const urlTemplateWithMeta = await geogridSource.getUrlTemplateWithMeta({
+ const urlTemplateWithMeta = await mvtGeogridSource.getUrlTemplateWithMeta({
...vectorSourceRequestMeta,
searchSessionId: '1',
});
expect(
urlTemplateWithMeta.urlTemplate.startsWith(
- "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628))),meta:(alias:!n,disabled:!f,key:bar,negate:!f)))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point"
+ "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point&searchSessionId=1"
)
).toBe(true);
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 7ed24b4805997..86dd63be4b67b 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
@@ -142,6 +142,20 @@ export class ESPewPewSource extends AbstractESAggSource {
},
});
+ // pewpew source is often used with security solution index-pattern
+ // Some underlying indices may not contain geo fields
+ // Filter out documents without geo fields to avoid shard failures for those indices
+ searchSource.setField('filter', [
+ ...searchSource.getField('filter'),
+ // destGeoField exists ensured by buffer filter
+ // so only need additional check for sourceGeoField
+ {
+ exists: {
+ field: this._descriptor.sourceGeoField,
+ },
+ },
+ ]);
+
const esResponse = await this._runEsQuery({
requestId: this.getId(),
requestName: layerName,
diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts
index 776d316440a56..82d2162986503 100644
--- a/x-pack/plugins/maps/server/mvt/get_tile.ts
+++ b/x-pack/plugins/maps/server/mvt/get_tile.ts
@@ -24,6 +24,7 @@ import {
} from '../../common/constants';
import {
+ createExtentFilter,
convertRegularRespToGeoJson,
hitsToGeoJson,
isTotalHitsGreaterThan,
@@ -254,21 +255,14 @@ export async function getTile({
}
function getTileSpatialFilter(geometryFieldName: string, tileBounds: ESBounds): unknown {
- return {
- geo_shape: {
- [geometryFieldName]: {
- shape: {
- type: 'envelope',
- // upper left and lower right points of the shape to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]]
- coordinates: [
- [tileBounds.top_left.lon, tileBounds.top_left.lat],
- [tileBounds.bottom_right.lon, tileBounds.bottom_right.lat],
- ],
- },
- relation: 'INTERSECTS',
- },
- },
+ const tileExtent = {
+ minLon: tileBounds.top_left.lon,
+ minLat: tileBounds.bottom_right.lat,
+ maxLon: tileBounds.bottom_right.lon,
+ maxLat: tileBounds.top_left.lat,
};
+ const tileExtentFilter = createExtentFilter(tileExtent, [geometryFieldName]);
+ return tileExtentFilter.query;
}
function esBboxToGeoJsonPolygon(esBounds: ESBounds, tileBounds: ESBounds): Polygon {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx
index bc1c9dbed1dcc..086adcecd077a 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx
@@ -17,7 +17,6 @@ import {
EuiDataGridPopoverContents,
EuiFlexGroup,
EuiFlexItem,
- EuiIconTip,
EuiSpacer,
EuiText,
EuiTitle,
@@ -51,6 +50,7 @@ import { isTrainingFilter } from './is_training_filter';
import { useRocCurve } from './use_roc_curve';
import { useConfusionMatrix } from './use_confusion_matrix';
import { MulticlassConfusionMatrixHelpPopover } from './confusion_matrix_help_popover';
+import { RocCurveHelpPopover } from './roc_curve_help_popover';
export interface EvaluatePanelProps {
jobConfig: DataFrameAnalyticsConfig;
@@ -409,7 +409,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se
{/* AUC ROC Chart */}
-
+
= ({ jobConfig, jobStatus, se
-
+
{Array.isArray(errorRocCurve) && (
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/roc_curve_help_popover.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/roc_curve_help_popover.tsx
new file mode 100644
index 0000000000000..f828cbabde894
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/roc_curve_help_popover.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ HelpPopover,
+ HelpPopoverButton,
+} from '../../../../../components/help_popover/help_popover';
+
+export const RocCurveHelpPopover = () => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ return (
+ {
+ setIsPopoverOpen(!isPopoverOpen);
+ }}
+ />
+ }
+ closePopover={() => setIsPopoverOpen(false)}
+ isOpen={isPopoverOpen}
+ title={i18n.translate('xpack.ml.dataframe.analytics.rocCurvePopoverTitle', {
+ defaultMessage: 'Receiver operating characteristic (ROC) curve',
+ })}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json
index ad7da3330bb6c..90f88275cb6d0 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json
@@ -22,7 +22,7 @@
],
"per_partition_categorization": {
"enabled": true,
- "stop_on_warn": false
+ "stop_on_warn": true
}
},
"analysis_limits": {
@@ -38,6 +38,6 @@
},
"custom_settings": {
"created_by": "ml-module-logs-ui-categories",
- "job_revision": 1
+ "job_revision": 2
}
}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/logo.json
new file mode 100755
index 0000000000000..862f970b7405d
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/logo.json
@@ -0,0 +1,3 @@
+{
+ "icon": "logoSecurity"
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json
new file mode 100755
index 0000000000000..480f49f3f2b19
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json
@@ -0,0 +1,77 @@
+{
+ "id": "security_auth",
+ "title": "Security: Authentication",
+ "description": "Detect anomalous activity in your ECS-compatible authentication logs.",
+ "type": "auth data",
+ "logoFile": "logo.json",
+ "defaultIndexPattern": "auditbeat-*,logs-*,filebeat-*,winlogbeat-*",
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ }
+ ]
+ }
+ },
+ "jobs": [
+ {
+ "id": "auth_high_count_logon_events_for_a_source_ip",
+ "file": "auth_high_count_logon_events_for_a_source_ip.json"
+ },
+ {
+ "id": "auth_high_count_logon_fails",
+ "file": "auth_high_count_logon_fails.json"
+ },
+ {
+ "id": "auth_high_count_logon_events",
+ "file": "auth_high_count_logon_events.json"
+ },
+ {
+ "id": "auth_rare_hour_for_a_user",
+ "file": "auth_rare_hour_for_a_user.json"
+ },
+ {
+ "id": "auth_rare_source_ip_for_a_user",
+ "file": "auth_rare_source_ip_for_a_user.json"
+ },
+ {
+ "id": "auth_rare_user",
+ "file": "auth_rare_user.json"
+ }
+ ],
+ "datafeeds": [
+ {
+ "id": "datafeed-auth_high_count_logon_events_for_a_source_ip",
+ "file": "datafeed_auth_high_count_logon_events_for_a_source_ip.json",
+ "job_id": "auth_high_count_logon_events_for_a_source_ip"
+ },
+ {
+ "id": "datafeed-auth_high_count_logon_fails",
+ "file": "datafeed_auth_high_count_logon_fails.json",
+ "job_id": "auth_high_count_logon_fails"
+ },
+ {
+ "id": "datafeed-auth_high_count_logon_events",
+ "file": "datafeed_auth_high_count_logon_events.json",
+ "job_id": "auth_high_count_logon_events"
+ },
+ {
+ "id": "datafeed-auth_rare_hour_for_a_user",
+ "file": "datafeed_auth_rare_hour_for_a_user.json",
+ "job_id": "auth_rare_hour_for_a_user"
+ },
+ {
+ "id": "datafeed-auth_rare_source_ip_for_a_user",
+ "file": "datafeed_auth_rare_source_ip_for_a_user.json",
+ "job_id": "auth_rare_source_ip_for_a_user"
+ },
+ {
+ "id": "datafeed-auth_rare_user",
+ "file": "datafeed_auth_rare_user.json",
+ "job_id": "auth_rare_user"
+ }
+ ]
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json
new file mode 100644
index 0000000000000..ee84fb222bb5c
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json
@@ -0,0 +1,29 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for an unusually large spike in successful authentication events. This can be due to password spraying, user enumeration or brute force activity.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "high count of logon events",
+ "function": "high_non_zero_count",
+ "detector_index": 0
+ }
+ ],
+ "influencers": []
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json
new file mode 100644
index 0000000000000..7bbbc81b6de7a
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json
@@ -0,0 +1,34 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for an unusually large spike in successful authentication events events from a particular source IP address. This can be due to password spraying, user enumeration or brute force activity.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "high count of auth events for a source IP",
+ "function": "high_non_zero_count",
+ "by_field_name": "source.ip",
+ "detector_index": 0
+ }
+ ],
+ "influencers": [
+ "source.ip",
+ "winlog.event_data.LogonType",
+ "user.name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json
new file mode 100644
index 0000000000000..4b7094e92c6ec
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json
@@ -0,0 +1,29 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for an unusually large spike in authentication failure events. This can be due to password spraying, user enumeration or brute force activity and may be a precursor to account takeover or credentialed access.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "high count of logon fails",
+ "function": "high_non_zero_count",
+ "detector_index": 0
+ }
+ ],
+ "influencers": []
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json
new file mode 100644
index 0000000000000..bb86d256e59df
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json
@@ -0,0 +1,33 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for a user logging in at a time of day that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different time zones. In addition, unauthorized user activity often takes place during non-business hours.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "rare hour for a user",
+ "function": "time_of_day",
+ "by_field_name": "user.name",
+ "detector_index": 0
+ }
+ ],
+ "influencers": [
+ "source.ip",
+ "user.name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json
new file mode 100644
index 0000000000000..6f72e148fa38e
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json
@@ -0,0 +1,34 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for a user logging in from an IP address that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different locations. An unusual source IP address for a username could also be due to lateral movement when a compromised account is used to pivot between hosts.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "rare source IP for a user",
+ "function": "rare",
+ "by_field_name": "source.ip",
+ "partition_field_name": "user.name",
+ "detector_index": 0
+ }
+ ],
+ "influencers": [
+ "source.ip",
+ "user.name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json
new file mode 100644
index 0000000000000..5cb9c7112b29d
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json
@@ -0,0 +1,33 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for an unusual user name in the authentication logs. An unusual user name is one way of detecting credentialed access by means of a new or dormant user account. A user account that is normally inactive, because the user has left the organization, which becomes active, may be due to credentialed access using a compromised account password. Threat actors will sometimes also create new users as a means of persisting in a compromised web application.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "rare user",
+ "function": "rare",
+ "by_field_name": "user.name",
+ "detector_index": 0
+ }
+ ],
+ "influencers": [
+ "source.ip",
+ "user.name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events.json
new file mode 100644
index 0000000000000..eb81179e44363
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_high_count_logon_events",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "success"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events_for_a_source_ip.json
new file mode 100644
index 0000000000000..dfed3ada1fe0b
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events_for_a_source_ip.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_high_count_logon_events_for_a_source_ip",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "success"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_fails.json
new file mode 100644
index 0000000000000..431c115b34d60
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_fails.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_high_count_logon_fails",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "failure"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_hour_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_hour_for_a_user.json
new file mode 100644
index 0000000000000..377197231f28c
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_hour_for_a_user.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_rare_hour_for_a_user",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "success"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_source_ip_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_source_ip_for_a_user.json
new file mode 100644
index 0000000000000..dfa2ad7ab397c
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_source_ip_for_a_user.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_rare_source_ip_for_a_user",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "success"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_user.json
new file mode 100644
index 0000000000000..f7de5d3aee71a
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_user.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_rare_user",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "success"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/saved_objects/checks.ts b/x-pack/plugins/ml/server/saved_objects/checks.ts
index 6b24ef000b695..48ceaefb23939 100644
--- a/x-pack/plugins/ml/server/saved_objects/checks.ts
+++ b/x-pack/plugins/ml/server/saved_objects/checks.ts
@@ -24,7 +24,7 @@ interface JobSavedObjectStatus {
};
}
-interface JobStatus {
+export interface JobStatus {
jobId: string;
datafeedId?: string | null;
checks: {
@@ -68,7 +68,9 @@ export function checksFactory(
if (type === 'anomaly-detector') {
jobExists = adJobs.jobs.some((j) => j.job_id === jobId);
- datafeedExists = datafeeds.datafeeds.some((d) => d.job_id === jobId);
+ datafeedExists = datafeeds.datafeeds.some(
+ (d) => d.datafeed_id === datafeedId && d.job_id === jobId
+ );
} else {
jobExists = dfaJobs.data_frame_analytics.some((j) => j.id === jobId);
}
diff --git a/x-pack/plugins/ml/server/saved_objects/sync.ts b/x-pack/plugins/ml/server/saved_objects/sync.ts
index a59687b9c3cbf..d6fa887d1d68b 100644
--- a/x-pack/plugins/ml/server/saved_objects/sync.ts
+++ b/x-pack/plugins/ml/server/saved_objects/sync.ts
@@ -14,6 +14,7 @@ import {
InitializeSavedObjectResponse,
} from '../../common/types/saved_objects';
import { checksFactory } from './checks';
+import type { JobStatus } from './checks';
import { getSavedObjectClientError } from './util';
import { Datafeed } from '../../common/types/anomaly_detection_jobs';
@@ -45,6 +46,12 @@ export function syncSavedObjectsFactory(
const tasks: Array<() => Promise> = [];
const status = await checkStatus();
+
+ const adJobsById = status.jobs['anomaly-detector'].reduce((acc, j) => {
+ acc[j.jobId] = j;
+ return acc;
+ }, {} as Record);
+
for (const job of status.jobs['anomaly-detector']) {
if (job.checks.savedObjectExits === false) {
if (simulate === true) {
@@ -141,8 +148,16 @@ export function syncSavedObjectsFactory(
}
for (const job of status.savedObjects['anomaly-detector']) {
- if (job.checks.datafeedExists === true && job.datafeedId === null) {
+ if (
+ (job.checks.datafeedExists === true && job.datafeedId === null) ||
+ (job.checks.datafeedExists === false &&
+ job.datafeedId === null &&
+ job.checks.datafeedExists === false &&
+ adJobsById[job.jobId] &&
+ adJobsById[job.jobId].datafeedId !== job.datafeedId)
+ ) {
// add datafeed id for jobs where the datafeed exists but the id is missing from the saved object
+ // or if the datafeed id in the saved object is not the same as the one attached to the job in es
if (simulate === true) {
results.datafeedsAdded[job.jobId] = { success: true };
} else {
diff --git a/x-pack/plugins/observability/public/application/types.ts b/x-pack/plugins/observability/public/application/types.ts
new file mode 100644
index 0000000000000..09c5de1e694c8
--- /dev/null
+++ b/x-pack/plugins/observability/public/application/types.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ ApplicationStart,
+ ChromeStart,
+ HttpStart,
+ IUiSettingsClient,
+ NotificationsStart,
+ OverlayStart,
+ SavedObjectsStart,
+} from 'kibana/public';
+import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public';
+import { NavigationPublicPluginStart } from 'src/plugins/navigation/public';
+import { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public';
+import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
+import { LensPublicStart } from '../../../lens/public';
+import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public';
+
+export interface ObservabilityAppServices {
+ http: HttpStart;
+ chrome: ChromeStart;
+ overlays: OverlayStart;
+ storage: IStorageWrapper;
+ data: DataPublicPluginStart;
+ uiSettings: IUiSettingsClient;
+ application: ApplicationStart;
+ notifications: NotificationsStart;
+ stateTransfer: EmbeddableStateTransfer;
+ navigation: NavigationPublicPluginStart;
+ savedObjectsClient: SavedObjectsStart['client'];
+
+ triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
+ lens: LensPublicStart;
+}
diff --git a/x-pack/plugins/observability/public/components/app/section/index.tsx b/x-pack/plugins/observability/public/components/app/section/index.tsx
index ddfd8ebed4f8f..cbe0c45bbd169 100644
--- a/x-pack/plugins/observability/public/components/app/section/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/section/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiAccordion, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+import { EuiAccordion, EuiPanel, EuiSpacer, EuiTitle, EuiButton } from '@elastic/eui';
import React from 'react';
import { ErrorPanel } from './error_panel';
import { usePluginContext } from '../../../hooks/use_plugin_context';
@@ -25,36 +25,29 @@ interface Props {
export function SectionContainer({ title, appLink, children, hasError }: Props) {
const { core } = usePluginContext();
return (
-
- {title}
-
- }
- extraAction={
- appLink?.href && (
-
- {appLink.label}
-
- )
- }
- >
- <>
-
-
- {hasError ? (
-
- ) : (
- <>
-
- {children}
- >
- )}
-
- >
-
+
+
+ {title}
+
+ }
+ extraAction={
+ appLink?.href && (
+ {appLink.label}
+ )
+ }
+ >
+ <>
+
+
+ {hasError ? : <>{children}>}
+
+ >
+
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
index c042ba9d0bcf8..7c772cb8dbdbc 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
@@ -26,7 +26,7 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={false}
- seriesId={'kpi-trends'}
+ seriesId={'kpi-over-time'}
removeFilter={jest.fn()}
/>
);
@@ -49,7 +49,7 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={false}
- seriesId={'kpi-trends'}
+ seriesId={'kpi-over-time'}
removeFilter={removeFilter}
/>
);
@@ -71,7 +71,7 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={false}
- seriesId={'kpi-trends'}
+ seriesId={'kpi-over-time'}
removeFilter={removeFilter}
/>
);
@@ -96,7 +96,7 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={true}
- seriesId={'kpi-trends'}
+ seriesId={'kpi-over-time'}
removeFilter={jest.fn()}
/>
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts
index 1c2627dac30e7..7c3abba3e5b05 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts
@@ -10,10 +10,9 @@ import { FieldLabels } from '../constants';
import { buildPhraseFilter } from '../utils';
import { TRANSACTION_DURATION } from '../constants/elasticsearch_fieldnames';
-export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
+export function getServiceLatencyLensConfig({ indexPattern }: ConfigProps): DataSeries {
return {
- id: seriesId,
- reportType: 'service-latency',
+ reportType: 'kpi-over-time',
defaultSeriesType: 'line',
seriesTypes: ['line', 'bar'],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts
deleted file mode 100644
index 2de2cbdfd75a6..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { ConfigProps, DataSeries } from '../../types';
-import { FieldLabels } from '../constants/constants';
-import { buildPhraseFilter } from '../utils';
-import { TRANSACTION_DURATION } from '../constants/elasticsearch_fieldnames';
-
-export function getServiceThroughputLensConfig({
- seriesId,
- indexPattern,
-}: ConfigProps): DataSeries {
- return {
- id: seriesId,
- reportType: 'service-throughput',
- defaultSeriesType: 'line',
- seriesTypes: ['line', 'bar'],
- xAxisColumn: {
- sourceField: '@timestamp',
- },
- yAxisColumns: [
- {
- operationType: 'average',
- sourceField: 'transaction.duration.us',
- label: 'Throughput',
- },
- ],
- hasOperationType: true,
- defaultFilters: [
- 'user_agent.name',
- 'user_agent.os.name',
- 'client.geo.country_name',
- 'user_agent.device.name',
- ],
- breakdowns: [
- 'user_agent.name',
- 'user_agent.os.name',
- 'client.geo.country_name',
- 'user_agent.device.name',
- ],
- filters: buildPhraseFilter('transaction.type', 'request', indexPattern),
- labels: { ...FieldLabels, [TRANSACTION_DURATION]: 'Throughput' },
- reportDefinitions: [
- {
- field: 'service.name',
- required: true,
- },
- {
- field: 'service.environment',
- },
- ],
- };
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
index e1142a071aab5..26459e676de08 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { AppDataType, ReportViewTypeId } from '../../types';
+import { ReportViewTypeId } from '../../types';
import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames';
import {
AGENT_HOST_LABEL,
@@ -13,7 +13,6 @@ import {
BROWSER_VERSION_LABEL,
CLS_LABEL,
CORE_WEB_VITALS_LABEL,
- CPU_USAGE_LABEL,
DEVICE_LABEL,
ENVIRONMENT_LABEL,
FCP_LABEL,
@@ -23,25 +22,18 @@ import {
KPI_LABEL,
LCP_LABEL,
LOCATION_LABEL,
- LOGS_FREQUENCY_LABEL,
- MEMORY_USAGE_LABEL,
METRIC_LABEL,
- MONITOR_DURATION_LABEL,
MONITOR_ID_LABEL,
MONITOR_NAME_LABEL,
MONITOR_STATUS_LABEL,
MONITOR_TYPE_LABEL,
- NETWORK_ACTIVITY_LABEL,
OBSERVER_LOCATION_LABEL,
OS_LABEL,
PERF_DIST_LABEL,
PORT_LABEL,
- SERVICE_LATENCY_LABEL,
SERVICE_NAME_LABEL,
- SERVICE_THROUGHPUT_LABEL,
TAGS_LABEL,
TBT_LABEL,
- UPTIME_PINGS_LABEL,
URL_LABEL,
} from './labels';
@@ -83,33 +75,11 @@ export const FieldLabels: Record = {
};
export const DataViewLabels: Record = {
- pld: PERF_DIST_LABEL,
- upd: MONITOR_DURATION_LABEL,
- upp: UPTIME_PINGS_LABEL,
- svl: SERVICE_LATENCY_LABEL,
+ dist: PERF_DIST_LABEL,
kpi: KIP_OVER_TIME_LABEL,
- tpt: SERVICE_THROUGHPUT_LABEL,
- cpu: CPU_USAGE_LABEL,
- logs: LOGS_FREQUENCY_LABEL,
- mem: MEMORY_USAGE_LABEL,
- nwk: NETWORK_ACTIVITY_LABEL,
cwv: CORE_WEB_VITALS_LABEL,
};
-export const ReportToDataTypeMap: Record = {
- upd: 'synthetics',
- upp: 'synthetics',
- tpt: 'apm',
- svl: 'apm',
- kpi: 'ux',
- pld: 'ux',
- nwk: 'infra_metrics',
- mem: 'infra_metrics',
- logs: 'infra_logs',
- cpu: 'infra_metrics',
- cwv: 'ux',
-};
-
export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN';
export const FILTER_RECORDS = 'FILTER_RECORDS';
export const OPERATION_COLUMN = 'operation';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts
new file mode 100644
index 0000000000000..edf8b7fb9d741
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const MONITOR_DURATION_US = 'monitor.duration.us';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts
index 92150a76319f8..b5816daa419df 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts
@@ -100,6 +100,10 @@ export const PAGES_LOADED_LABEL = i18n.translate(
}
);
+export const PINGS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.pings', {
+ defaultMessage: 'Pings',
+});
+
export const MONITOR_ID_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.monitorId',
{
@@ -165,42 +169,6 @@ export const PERF_DIST_LABEL = i18n.translate(
}
);
-export const MONITOR_DURATION_LABEL = i18n.translate(
- 'xpack.observability.expView.fieldLabels.monitorDuration',
- {
- defaultMessage: 'Uptime monitor duration',
- }
-);
-
-export const UPTIME_PINGS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.pings', {
- defaultMessage: 'Uptime pings',
-});
-
-export const SERVICE_LATENCY_LABEL = i18n.translate(
- 'xpack.observability.expView.fieldLabels.serviceLatency',
- {
- defaultMessage: 'APM Service latency',
- }
-);
-
-export const SERVICE_THROUGHPUT_LABEL = i18n.translate(
- 'xpack.observability.expView.fieldLabels.serviceThroughput',
- {
- defaultMessage: 'APM Service throughput',
- }
-);
-
-export const CPU_USAGE_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.cpuUsage', {
- defaultMessage: 'System CPU usage',
-});
-
-export const NETWORK_ACTIVITY_LABEL = i18n.translate(
- 'xpack.observability.expView.fieldLabels.networkActivity',
- {
- defaultMessage: 'Network activity',
- }
-);
-
export const CORE_WEB_VITALS_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.coreWebVitals',
{
@@ -215,13 +183,6 @@ export const MEMORY_USAGE_LABEL = i18n.translate(
}
);
-export const LOGS_FREQUENCY_LABEL = i18n.translate(
- 'xpack.observability.expView.fieldLabels.logsFrequency',
- {
- defaultMessage: 'Logs frequency',
- }
-);
-
export const KIP_OVER_TIME_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.kpiOverTime',
{
@@ -243,10 +204,10 @@ export const WEB_APPLICATION_LABEL = i18n.translate(
}
);
-export const UP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.up', {
- defaultMessage: 'Up',
+export const UP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.upPings', {
+ defaultMessage: 'Up Pings',
});
-export const DOWN_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.down', {
- defaultMessage: 'Down',
+export const DOWN_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.downPings', {
+ defaultMessage: 'Down Pings',
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
index 797ee0c2e0977..13a7900ef5764 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
@@ -5,51 +5,37 @@
* 2.0.
*/
-import { ReportViewTypes } from '../types';
-import { getPerformanceDistLensConfig } from './rum/performance_dist_config';
-import { getMonitorDurationConfig } from './synthetics/monitor_duration_config';
-import { getServiceLatencyLensConfig } from './apm/service_latency_config';
-import { getMonitorPingsConfig } from './synthetics/monitor_pings_config';
-import { getServiceThroughputLensConfig } from './apm/service_throughput_config';
-import { getKPITrendsLensConfig } from './rum/kpi_trends_config';
-import { getCPUUsageLensConfig } from './metrics/cpu_usage_config';
-import { getMemoryUsageLensConfig } from './metrics/memory_usage_config';
-import { getNetworkActivityLensConfig } from './metrics/network_activity_config';
-import { getLogsFrequencyLensConfig } from './logs/logs_frequency_config';
-import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
+import { AppDataType, ReportViewTypes } from '../types';
+import { getRumDistributionConfig } from './rum/data_distribution_config';
+import { getSyntheticsDistributionConfig } from './synthetics/data_distribution_config';
+import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config';
+import { getKPITrendsLensConfig } from './rum/kpi_over_time_config';
+import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config';
interface Props {
reportType: keyof typeof ReportViewTypes;
- seriesId: string;
- indexPattern: IIndexPattern;
+ indexPattern: IndexPattern;
+ dataType: AppDataType;
}
-export const getDefaultConfigs = ({ reportType, seriesId, indexPattern }: Props) => {
- switch (ReportViewTypes[reportType]) {
- case 'page-load-dist':
- return getPerformanceDistLensConfig({ seriesId, indexPattern });
- case 'kpi-trends':
- return getKPITrendsLensConfig({ seriesId, indexPattern });
- case 'core-web-vitals':
- return getCoreWebVitalsConfig({ seriesId, indexPattern });
- case 'uptime-duration':
- return getMonitorDurationConfig({ seriesId, indexPattern });
- case 'uptime-pings':
- return getMonitorPingsConfig({ seriesId, indexPattern });
- case 'service-latency':
- return getServiceLatencyLensConfig({ seriesId, indexPattern });
- case 'service-throughput':
- return getServiceThroughputLensConfig({ seriesId, indexPattern });
- case 'cpu-usage':
- return getCPUUsageLensConfig({ seriesId });
- case 'memory-usage':
- return getMemoryUsageLensConfig({ seriesId });
- case 'network-activity':
- return getNetworkActivityLensConfig({ seriesId });
- case 'logs-frequency':
- return getLogsFrequencyLensConfig({ seriesId });
+export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => {
+ switch (dataType) {
+ case 'ux':
+ if (reportType === 'dist') {
+ return getRumDistributionConfig({ indexPattern });
+ }
+ if (reportType === 'cwv') {
+ return getCoreWebVitalsConfig({ indexPattern });
+ }
+ return getKPITrendsLensConfig({ indexPattern });
+ case 'synthetics':
+ if (reportType === 'dist') {
+ return getSyntheticsDistributionConfig({ indexPattern });
+ }
+ return getSyntheticsKPIConfig({ indexPattern });
+
default:
- return getKPITrendsLensConfig({ seriesId, indexPattern });
+ return getKPITrendsLensConfig({ indexPattern });
}
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
index 6976b55921b09..8b21df64a3c91 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
@@ -15,9 +15,9 @@ describe('Lens Attribute', () => {
mockAppIndexPattern();
const reportViewConfig = getDefaultConfigs({
- reportType: 'pld',
+ reportType: 'dist',
+ dataType: 'ux',
indexPattern: mockIndexPattern,
- seriesId: 'series-id',
});
let lnsAttr: LensAttributes;
@@ -73,6 +73,7 @@ describe('Lens Attribute', () => {
readFromDocValues: true,
},
fieldName: 'transaction.duration.us',
+ columnLabel: 'Page load time',
})
);
});
@@ -95,6 +96,7 @@ describe('Lens Attribute', () => {
readFromDocValues: true,
},
fieldName: LCP_FIELD,
+ columnLabel: 'Largest contentful paint',
})
);
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
index 89fc9ca5fcc58..bc535e29ab435 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
@@ -55,6 +55,7 @@ export const parseCustomFieldName = (
let fieldName = sourceField;
let columnType;
let columnFilters;
+ let columnLabel;
const rdf = reportViewConfig.reportDefinitions ?? [];
@@ -69,15 +70,17 @@ export const parseCustomFieldName = (
);
columnType = currField?.columnType;
columnFilters = currField?.columnFilters;
+ columnLabel = currField?.label;
}
} else if (customField.options?.[0].field || customField.options?.[0].id) {
fieldName = customField.options?.[0].field || customField.options?.[0].id;
columnType = customField.options?.[0].columnType;
columnFilters = customField.options?.[0].columnFilters;
+ columnLabel = customField.options?.[0].label;
}
}
- return { fieldName, columnType, columnFilters };
+ return { fieldName, columnType, columnFilters, columnLabel };
};
export class LensAttributes {
@@ -260,12 +263,14 @@ export class LensAttributes {
label?: string,
colIndex?: number
) {
- const { fieldMeta, columnType, fieldName, columnFilters } = this.getFieldMeta(sourceField);
+ const { fieldMeta, columnType, fieldName, columnFilters, columnLabel } = this.getFieldMeta(
+ sourceField
+ );
const { type: fieldType } = fieldMeta ?? {};
if (fieldName === 'Records' || columnType === FILTER_RECORDS) {
return this.getRecordsColumn(
- label,
+ columnLabel || label,
colIndex !== undefined ? columnFilters?.[colIndex] : undefined
);
}
@@ -274,7 +279,7 @@ export class LensAttributes {
return this.getDateHistogramColumn(fieldName);
}
if (fieldType === 'number') {
- return this.getNumberColumn(fieldName, columnType, operationType, label);
+ return this.getNumberColumn(fieldName, columnType, operationType, columnLabel || label);
}
// FIXME review my approach again
@@ -286,11 +291,13 @@ export class LensAttributes {
}
getFieldMeta(sourceField: string) {
- const { fieldName, columnType, columnFilters } = this.getCustomFieldName(sourceField);
+ const { fieldName, columnType, columnFilters, columnLabel } = this.getCustomFieldName(
+ sourceField
+ );
const fieldMeta = this.indexPattern.getFieldByName(fieldName);
- return { fieldMeta, fieldName, columnType, columnFilters };
+ return { fieldMeta, fieldName, columnType, columnFilters, columnLabel };
}
getMainYAxis() {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts
deleted file mode 100644
index 97d915ede01a9..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { DataSeries } from '../../types';
-import { FieldLabels } from '../constants';
-
-interface Props {
- seriesId: string;
-}
-
-export function getLogsFrequencyLensConfig({ seriesId }: Props): DataSeries {
- return {
- id: seriesId,
- reportType: 'logs-frequency',
- defaultSeriesType: 'line',
- seriesTypes: ['line', 'bar'],
- xAxisColumn: {
- sourceField: '@timestamp',
- },
- yAxisColumns: [
- {
- operationType: 'count',
- },
- ],
- hasOperationType: false,
- defaultFilters: [],
- breakdowns: ['agent.hostname'],
- filters: [],
- labels: { ...FieldLabels },
- reportDefinitions: [
- {
- field: 'agent.hostname',
- required: true,
- },
- ],
- };
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts
index 28b381bd12473..2d44e122af82d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts
@@ -5,17 +5,12 @@
* 2.0.
*/
-import { DataSeries } from '../../types';
+import { DataSeries, ConfigProps } from '../../types';
import { FieldLabels } from '../constants';
-interface Props {
- seriesId: string;
-}
-
-export function getCPUUsageLensConfig({ seriesId }: Props): DataSeries {
+export function getCPUUsageLensConfig({}: ConfigProps): DataSeries {
return {
- id: seriesId,
- reportType: 'cpu-usage',
+ reportType: 'kpi-over-time',
defaultSeriesType: 'line',
seriesTypes: ['line', 'bar'],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts
index 2bd0e4b032778..deaa551dce657 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts
@@ -5,17 +5,12 @@
* 2.0.
*/
-import { DataSeries } from '../../types';
+import { DataSeries, ConfigProps } from '../../types';
import { FieldLabels } from '../constants';
-interface Props {
- seriesId: string;
-}
-
-export function getMemoryUsageLensConfig({ seriesId }: Props): DataSeries {
+export function getMemoryUsageLensConfig({}: ConfigProps): DataSeries {
return {
- id: seriesId,
- reportType: 'memory-usage',
+ reportType: 'kpi-over-time',
defaultSeriesType: 'line',
seriesTypes: ['line', 'bar'],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts
index 924701bc13490..d27cdba207d63 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts
@@ -5,17 +5,12 @@
* 2.0.
*/
-import { DataSeries } from '../../types';
+import { DataSeries, ConfigProps } from '../../types';
import { FieldLabels } from '../constants';
-interface Props {
- seriesId: string;
-}
-
-export function getNetworkActivityLensConfig({ seriesId }: Props): DataSeries {
+export function getNetworkActivityLensConfig({}: ConfigProps): DataSeries {
return {
- id: seriesId,
- reportType: 'network-activity',
+ reportType: 'kpi-over-time',
defaultSeriesType: 'line',
seriesTypes: ['line', 'bar'],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts
index de9ea12be20cf..e34d8b0dcfdd0 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts
@@ -27,13 +27,12 @@ import {
SERVICE_ENVIRONMENT,
} from '../constants/elasticsearch_fieldnames';
-export function getCoreWebVitalsConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
+export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): DataSeries {
const statusPallete = euiPaletteForStatus(3);
return {
- id: seriesId,
defaultSeriesType: 'bar_horizontal_percentage_stacked',
- reportType: 'kpi-trends',
+ reportType: 'core-web-vitals',
seriesTypes: ['bar_horizontal_percentage_stacked'],
xAxisColumn: {
sourceField: USE_BREAK_DOWN_COLUMN,
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts
similarity index 94%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts
index 4a1521c834806..812f1b2e4cf33 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts
@@ -39,10 +39,9 @@ import {
WEB_APPLICATION_LABEL,
} from '../constants/labels';
-export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
+export function getRumDistributionConfig({ indexPattern }: ConfigProps): DataSeries {
return {
- id: seriesId ?? 'unique-key',
- reportType: 'page-load-dist',
+ reportType: 'data-distribution',
defaultSeriesType: 'line',
seriesTypes: [],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts
similarity index 95%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts
index f6c683caaa039..12d66c55c7d00 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts
@@ -39,12 +39,11 @@ import {
WEB_APPLICATION_LABEL,
} from '../constants/labels';
-export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
+export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): DataSeries {
return {
- id: seriesId,
defaultSeriesType: 'bar_stacked',
- reportType: 'kpi-trends',
seriesTypes: [],
+ reportType: 'kpi-over-time',
xAxisColumn: {
sourceField: '@timestamp',
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
similarity index 61%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
index 5e8a43ccf2ef4..854f844db047d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
@@ -6,27 +6,25 @@
*/
import { ConfigProps, DataSeries } from '../../types';
-import { FieldLabels } from '../constants';
+import { FieldLabels, RECORDS_FIELD } from '../constants';
import { buildExistsFilter } from '../utils';
-import { MONITORS_DURATION_LABEL } from '../constants/labels';
+import { MONITORS_DURATION_LABEL, PINGS_LABEL } from '../constants/labels';
-export function getMonitorDurationConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
+export function getSyntheticsDistributionConfig({ indexPattern }: ConfigProps): DataSeries {
return {
- id: seriesId,
- reportType: 'uptime-duration',
+ reportType: 'data-distribution',
defaultSeriesType: 'line',
seriesTypes: [],
xAxisColumn: {
- sourceField: '@timestamp',
+ sourceField: 'performance.metric',
},
yAxisColumns: [
{
- operationType: 'average',
- sourceField: 'monitor.duration.us',
- label: MONITORS_DURATION_LABEL,
+ sourceField: RECORDS_FIELD,
+ label: PINGS_LABEL,
},
],
- hasOperationType: true,
+ hasOperationType: false,
defaultFilters: ['monitor.type', 'observer.geo.name', 'tags'],
breakdowns: [
'observer.geo.name',
@@ -44,6 +42,13 @@ export function getMonitorDurationConfig({ seriesId, indexPattern }: ConfigProps
{
field: 'url.full',
},
+ {
+ field: 'performance.metric',
+ custom: true,
+ options: [
+ { label: 'Monitor duration', id: 'monitor.duration.us', field: 'monitor.duration.us' },
+ ],
+ },
],
labels: { ...FieldLabels, 'monitor.duration.us': MONITORS_DURATION_LABEL },
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts
new file mode 100644
index 0000000000000..3e92845436363
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ConfigProps, DataSeries } from '../../types';
+import { FieldLabels, OPERATION_COLUMN } from '../constants';
+import { buildExistsFilter } from '../utils';
+import { DOWN_LABEL, MONITORS_DURATION_LABEL, UP_LABEL } from '../constants/labels';
+import { MONITOR_DURATION_US } from '../constants/field_names/synthetics';
+const SUMMARY_UP = 'summary.up';
+const SUMMARY_DOWN = 'summary.down';
+
+export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): DataSeries {
+ return {
+ reportType: 'kpi-over-time',
+ defaultSeriesType: 'bar_stacked',
+ seriesTypes: [],
+ xAxisColumn: {
+ sourceField: '@timestamp',
+ },
+ yAxisColumns: [
+ {
+ sourceField: 'business.kpi',
+ operationType: 'median',
+ },
+ ],
+ hasOperationType: false,
+ defaultFilters: ['observer.geo.name', 'monitor.type', 'tags'],
+ breakdowns: ['observer.geo.name', 'monitor.type'],
+ filters: [...buildExistsFilter('summary.up', indexPattern)],
+ palette: { type: 'palette', name: 'status' },
+ reportDefinitions: [
+ {
+ field: 'monitor.name',
+ },
+ {
+ field: 'url.full',
+ },
+ {
+ field: 'business.kpi',
+ custom: true,
+ options: [
+ {
+ label: MONITORS_DURATION_LABEL,
+ field: MONITOR_DURATION_US,
+ id: MONITOR_DURATION_US,
+ columnType: OPERATION_COLUMN,
+ },
+ {
+ field: SUMMARY_UP,
+ id: SUMMARY_UP,
+ label: UP_LABEL,
+ columnType: OPERATION_COLUMN,
+ },
+ {
+ field: SUMMARY_DOWN,
+ id: SUMMARY_DOWN,
+ label: DOWN_LABEL,
+ columnType: OPERATION_COLUMN,
+ },
+ ],
+ },
+ ],
+ labels: { ...FieldLabels },
+ };
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts
deleted file mode 100644
index 697a940f666f7..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { ConfigProps, DataSeries } from '../../types';
-import { FieldLabels } from '../constants';
-import { buildExistsFilter } from '../utils';
-import { DOWN_LABEL, UP_LABEL } from '../constants/labels';
-
-export function getMonitorPingsConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
- return {
- id: seriesId,
- reportType: 'uptime-pings',
- defaultSeriesType: 'bar_stacked',
- seriesTypes: [],
- xAxisColumn: {
- sourceField: '@timestamp',
- },
- yAxisColumns: [
- {
- operationType: 'sum',
- sourceField: 'summary.up',
- label: UP_LABEL,
- },
- {
- operationType: 'sum',
- sourceField: 'summary.down',
- label: DOWN_LABEL,
- },
- ],
- yTitle: 'Pings',
- hasOperationType: false,
- defaultFilters: ['observer.geo.name', 'monitor.type', 'tags'],
- breakdowns: ['observer.geo.name', 'monitor.type'],
- filters: [...buildExistsFilter('summary.up', indexPattern)],
- palette: { type: 'palette', name: 'status' },
- reportDefinitions: [
- {
- field: 'monitor.name',
- },
- {
- field: 'url.full',
- },
- ],
- labels: { ...FieldLabels },
- };
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
index fc0062694e0a3..487ecdb2bafcc 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
@@ -52,7 +52,7 @@ describe('ExploratoryView', () => {
data: {
'ux-series': {
dataType: 'ux' as const,
- reportType: 'pld' as const,
+ reportType: 'dist' as const,
breakdown: 'user_agent .name',
reportDefinitions: { 'service.name': ['elastic-co'] },
time: { from: 'now-15m', to: 'now' },
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
index 7958dca6e396e..329ed20ffed3d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
@@ -16,7 +16,6 @@ import { useLensAttributes } from './hooks/use_lens_attributes';
import { EmptyView } from './components/empty_view';
import { TypedLensByValueInput } from '../../../../../lens/public';
import { useAppIndexPatternContext } from './hooks/use_app_index_pattern';
-import { ReportToDataTypeMap } from './configurations/constants';
import { SeriesBuilder } from './series_builder/series_builder';
export function ExploratoryView({
@@ -61,10 +60,10 @@ export function ExploratoryView({
};
useEffect(() => {
- if (series?.reportType || series?.dataType) {
- loadIndexPattern({ dataType: series?.dataType ?? ReportToDataTypeMap[series?.reportType] });
+ if (series?.dataType) {
+ loadIndexPattern({ dataType: series?.dataType });
}
- }, [series?.reportType, series?.dataType, loadIndexPattern]);
+ }, [series?.dataType, loadIndexPattern]);
useEffect(() => {
setLensAttributes(lensAttributesT);
@@ -91,7 +90,7 @@ export function ExploratoryView({
timeRange={series?.time}
attributes={lensAttributes}
onBrushEnd={({ range }) => {
- if (series?.reportType !== 'pld') {
+ if (series?.reportType !== 'dist') {
setSeries(seriesId, {
...series,
time: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx
index ca9f2c9e73eb8..1dedc4142f174 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx
@@ -26,7 +26,7 @@ describe('ExploratoryViewHeader', function () {
data: {
'uptime-pings-histogram': {
dataType: 'synthetics' as const,
- reportType: 'upp' as const,
+ reportType: 'kpi' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
index 3265287a7f915..3e02207e26272 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
@@ -5,13 +5,13 @@
* 2.0.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
-import { TypedLensByValueInput } from '../../../../../../lens/public';
+import { TypedLensByValueInput, LensEmbeddableInput } from '../../../../../../lens/public';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
-import { ObservabilityPublicPluginsStart } from '../../../../plugin';
import { DataViewLabels } from '../configurations/constants';
+import { ObservabilityAppServices } from '../../../../application/types';
import { useSeriesStorage } from '../hooks/use_series_storage';
interface Props {
@@ -20,57 +20,88 @@ interface Props {
}
export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
- const {
- services: { lens },
- } = useKibana();
+ const kServices = useKibana().services;
+
+ const { lens } = kServices;
const { getSeries } = useSeriesStorage();
const series = getSeries(seriesId);
+ const [isSaveOpen, setIsSaveOpen] = useState(false);
+
+ const LensSaveModalComponent = lens.SaveModalComponent;
+
return (
-
-
-
-
- {DataViewLabels[series.reportType] ??
- i18n.translate('xpack.observability.expView.heading.label', {
- defaultMessage: 'Analyze data',
- })}{' '}
-
-
-
-
-
- {
- if (lensAttributes) {
- lens.navigateToPrefilledEditor(
- {
- id: '',
- timeRange: series.time,
- attributes: lensAttributes,
- },
- true
- );
- }
- }}
- >
- {i18n.translate('xpack.observability.expView.heading.openInLens', {
- defaultMessage: 'Open in Lens',
- })}
-
-
-
+ <>
+
+
+
+
+ {DataViewLabels[series.reportType] ??
+ i18n.translate('xpack.observability.expView.heading.label', {
+ defaultMessage: 'Analyze data',
+ })}{' '}
+
+
+
+
+
+ {
+ if (lensAttributes) {
+ lens.navigateToPrefilledEditor(
+ {
+ id: '',
+ timeRange: series.time,
+ attributes: lensAttributes,
+ },
+ true
+ );
+ }
+ }}
+ >
+ {i18n.translate('xpack.observability.expView.heading.openInLens', {
+ defaultMessage: 'Open in Lens',
+ })}
+
+
+
+ {
+ if (lensAttributes) {
+ setIsSaveOpen(true);
+ }
+ }}
+ >
+ {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', {
+ defaultMessage: 'Save',
+ })}
+
+
+
+
+ {isSaveOpen && lensAttributes && (
+ setIsSaveOpen(false)}
+ onSave={() => {}}
+ />
+ )}
+ >
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
index 4e9c360745b6b..1c85bc5089b2a 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
@@ -42,7 +42,8 @@ export const useLensAttributes = ({
}: Props): TypedLensByValueInput['attributes'] | null => {
const { getSeries } = useSeriesStorage();
const series = getSeries(seriesId);
- const { breakdown, seriesType, operationType, reportType, reportDefinitions = {} } = series ?? {};
+ const { breakdown, seriesType, operationType, reportType, dataType, reportDefinitions = {} } =
+ series ?? {};
const { indexPattern } = useAppIndexPatternContext();
@@ -52,8 +53,8 @@ export const useLensAttributes = ({
}
const dataViewConfig = getDefaultConfigs({
- seriesId,
reportType,
+ dataType,
indexPattern,
});
@@ -78,12 +79,12 @@ export const useLensAttributes = ({
return lensAttributes.getJSON();
}, [
indexPattern,
- breakdown,
- seriesType,
- operationType,
reportType,
reportDefinitions,
- seriesId,
+ dataType,
series.filters,
+ seriesType,
+ operationType,
+ breakdown,
]);
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
index 9118e49a42dfb..ff766f7e6a1cf 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
@@ -259,7 +259,7 @@ function mockSeriesStorageContext({
}) {
const mockDataSeries = data || {
'performance-distribution': {
- reportType: 'pld',
+ reportType: 'dist',
dataType: 'ux',
breakdown: breakdown || 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
index 51529a3b1ac17..e3c1666c533ef 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
@@ -37,7 +37,7 @@ describe('DataTypesCol', function () {
data: {
[seriesId]: {
dataType: 'synthetics' as const,
- reportType: 'upp' as const,
+ reportType: 'kpi' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
index 08e7f4ddcd3d0..3fe88de518f75 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
@@ -11,7 +11,6 @@ import styled from 'styled-components';
import { AppDataType } from '../../types';
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
import { useSeriesStorage } from '../../hooks/use_series_storage';
-import { ReportToDataTypeMap } from '../../configurations/constants';
export const dataTypes: Array<{ id: AppDataType; label: string }> = [
{ id: 'synthetics', label: 'Synthetic Monitoring' },
@@ -35,7 +34,7 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) {
}
};
- const selectedDataType = series.dataType ?? ReportToDataTypeMap[series.reportType];
+ const selectedDataType = series.dataType;
return (
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx
index fa273f6180935..fce1383f30f34 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx
@@ -48,6 +48,12 @@ export function OperationTypeSelect({
defaultMessage: 'Median',
}),
},
+ {
+ value: 'sum' as OperationType,
+ inputDisplay: i18n.translate('xpack.observability.expView.operationType.sum', {
+ defaultMessage: 'Sum',
+ }),
+ },
{
value: '75th' as OperationType,
inputDisplay: i18n.translate('xpack.observability.expView.operationType.75thPercentile', {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
index f576862f18e76..805186e877d57 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
@@ -15,8 +15,8 @@ import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fiel
describe('Series Builder ReportBreakdowns', function () {
const seriesId = 'test-series-id';
const dataViewSeries = getDefaultConfigs({
- seriesId,
- reportType: 'pld',
+ reportType: 'dist',
+ dataType: 'ux',
indexPattern: mockIndexPattern,
});
@@ -45,7 +45,7 @@ describe('Series Builder ReportBreakdowns', function () {
expect(setSeries).toHaveBeenCalledWith(seriesId, {
breakdown: USER_AGENT_OS,
dataType: 'ux',
- reportType: 'pld',
+ reportType: 'dist',
time: { from: 'now-15m', to: 'now' },
});
});
@@ -67,7 +67,7 @@ describe('Series Builder ReportBreakdowns', function () {
expect(setSeries).toHaveBeenCalledWith(seriesId, {
breakdown: undefined,
dataType: 'ux',
- reportType: 'pld',
+ reportType: 'dist',
time: { from: 'now-15m', to: 'now' },
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
index fdf6633c0ddb5..8738235f0c54b 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
@@ -22,16 +22,16 @@ describe('Series Builder ReportDefinitionCol', function () {
const seriesId = 'test-series-id';
const dataViewSeries = getDefaultConfigs({
- seriesId,
- reportType: 'pld',
+ reportType: 'dist',
indexPattern: mockIndexPattern,
+ dataType: 'ux',
});
const initSeries = {
data: {
[seriesId]: {
dataType: 'ux' as const,
- reportType: 'pld' as const,
+ reportType: 'dist' as const,
time: { from: 'now-30d', to: 'now' },
reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
},
@@ -81,7 +81,7 @@ describe('Series Builder ReportDefinitionCol', function () {
expect(setSeries).toHaveBeenCalledWith(seriesId, {
dataType: 'ux',
reportDefinitions: {},
- reportType: 'pld',
+ reportType: 'dist',
time: { from: 'now-30d', to: 'now' },
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
index dc2dc629cc121..7ca947fed0bc9 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
@@ -15,9 +15,9 @@ describe('Series Builder ReportFilters', function () {
const seriesId = 'test-series-id';
const dataViewSeries = getDefaultConfigs({
- seriesId,
- reportType: 'pld',
+ reportType: 'dist',
indexPattern: mockIndexPattern,
+ dataType: 'ux',
});
it('should render properly', function () {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
index c721a2fa2fe77..f36d64ca5bbbd 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
@@ -34,13 +34,13 @@ describe('ReportTypesCol', function () {
);
- fireEvent.click(screen.getByText(/monitor duration/i));
+ fireEvent.click(screen.getByText(/KPI over time/i));
expect(setSeries).toHaveBeenCalledWith(seriesId, {
breakdown: 'user_agent.name',
dataType: 'ux',
reportDefinitions: {},
- reportType: 'upd',
+ reportType: 'kpi',
time: { from: 'now-15m', to: 'now' },
});
expect(setSeries).toHaveBeenCalledTimes(1);
@@ -51,7 +51,7 @@ describe('ReportTypesCol', function () {
data: {
[NEW_SERIES_KEY]: {
dataType: 'synthetics' as const,
- reportType: 'upp' as const,
+ reportType: 'kpi' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
},
@@ -64,7 +64,7 @@ describe('ReportTypesCol', function () {
);
const button = screen.getByRole('button', {
- name: /pings histogram/i,
+ name: /KPI over time/i,
});
expect(button.classList).toContain('euiButton--fill');
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
index 32f1fb7f7c43b..e24d246d60e58 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
@@ -21,29 +21,17 @@ import { getDefaultConfigs } from '../configurations/default_configs';
export const ReportTypes: Record> = {
synthetics: [
- { id: 'upd', label: 'Monitor duration' },
- { id: 'upp', label: 'Pings histogram' },
+ { id: 'kpi', label: 'KPI over time' },
+ { id: 'dist', label: 'Performance distribution' },
],
ux: [
- { id: 'pld', label: 'Performance distribution' },
{ id: 'kpi', label: 'KPI over time' },
+ { id: 'dist', label: 'Performance distribution' },
{ id: 'cwv', label: 'Core Web Vitals' },
],
- apm: [
- { id: 'svl', label: 'Latency' },
- { id: 'tpt', label: 'Throughput' },
- ],
- infra_logs: [
- {
- id: 'logs',
- label: 'Logs Frequency',
- },
- ],
- infra_metrics: [
- { id: 'cpu', label: 'CPU usage' },
- { id: 'mem', label: 'Memory usage' },
- { id: 'nwk', label: 'Network activity' },
- ],
+ apm: [],
+ infra_logs: [],
+ infra_metrics: [],
};
export function SeriesBuilder({
@@ -72,7 +60,7 @@ export function SeriesBuilder({
const getDataViewSeries = () => {
return getDefaultConfigs({
- seriesId,
+ dataType,
indexPattern,
reportType: reportType!,
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx
index 0edc4330ef97a..2b46bb9a8cd62 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx
@@ -17,7 +17,7 @@ describe('SeriesDatePicker', function () {
data: {
'uptime-pings-histogram': {
dataType: 'synthetics' as const,
- reportType: 'upp' as const,
+ reportType: 'dist' as const,
breakdown: 'monitor.status',
time: { from: 'now-30m', to: 'now' },
},
@@ -32,7 +32,7 @@ describe('SeriesDatePicker', function () {
const initSeries = {
data: {
'uptime-pings-histogram': {
- reportType: 'upp' as const,
+ reportType: 'kpi' as const,
dataType: 'synthetics' as const,
breakdown: 'monitor.status',
},
@@ -46,7 +46,7 @@ describe('SeriesDatePicker', function () {
expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', {
breakdown: 'monitor.status',
dataType: 'synthetics' as const,
- reportType: 'upp' as const,
+ reportType: 'kpi' as const,
time: DEFAULT_TIME,
});
});
@@ -56,7 +56,7 @@ describe('SeriesDatePicker', function () {
data: {
'uptime-pings-histogram': {
dataType: 'synthetics' as const,
- reportType: 'upp' as const,
+ reportType: 'kpi' as const,
breakdown: 'monitor.status',
time: { from: 'now-30m', to: 'now' },
},
@@ -79,7 +79,7 @@ describe('SeriesDatePicker', function () {
expect(setSeries).toHaveBeenCalledWith('series-id', {
breakdown: 'monitor.status',
dataType: 'synthetics',
- reportType: 'upp',
+ reportType: 'kpi',
time: { from: 'now/d', to: 'now/d' },
});
expect(setSeries).toHaveBeenCalledTimes(1);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx
index 4bef3e8f71821..a0d2fd86482a5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx
@@ -23,7 +23,7 @@ export function ChartEditOptions({ series, seriesId, breakdowns }: Props) {
-
+
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
index 0ce9db73f92b1..1d552486921e1 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
@@ -9,15 +9,14 @@ import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { Breakdowns } from './breakdowns';
import { mockIndexPattern, render } from '../../rtl_helpers';
-import { NEW_SERIES_KEY } from '../../hooks/use_series_storage';
import { getDefaultConfigs } from '../../configurations/default_configs';
import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames';
describe('Breakdowns', function () {
const dataViewSeries = getDefaultConfigs({
- reportType: 'pld',
+ reportType: 'dist',
indexPattern: mockIndexPattern,
- seriesId: NEW_SERIES_KEY,
+ dataType: 'ux',
});
it('should render properly', async function () {
@@ -53,7 +52,7 @@ describe('Breakdowns', function () {
expect(setSeries).toHaveBeenCalledWith('series-id', {
breakdown: 'user_agent.name',
dataType: 'ux',
- reportType: 'pld',
+ reportType: 'dist',
time: { from: 'now-15m', to: 'now' },
});
expect(setSeries).toHaveBeenCalledTimes(1);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx
index 975817a8417de..08664ac75eb8d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx
@@ -13,17 +13,18 @@ import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types
interface Props {
series: DataSeries;
+ seriesId: string;
}
-export function ChartOptions({ series }: Props) {
+export function ChartOptions({ series, seriesId }: Props) {
return (
-
+
{series.hasOperationType && (
-
+
)}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
index 5374fc33093a1..086a1d4341bbc 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
@@ -10,7 +10,6 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RemoveSeries } from './remove_series';
import { NEW_SERIES_KEY, useSeriesStorage } from '../../hooks/use_series_storage';
-import { ReportToDataTypeMap } from '../../configurations/constants';
interface Props {
seriesId: string;
@@ -21,7 +20,7 @@ export function SeriesActions({ seriesId }: Props) {
const onEdit = () => {
removeSeries(seriesId);
- setSeries(NEW_SERIES_KEY, { ...series, dataType: ReportToDataTypeMap[series.reportType] });
+ setSeries(NEW_SERIES_KEY, { ...series });
};
return (
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
index 2919714ac0cd4..8363b6b0eadfd 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
@@ -10,16 +10,15 @@ import { screen, waitFor } from '@testing-library/react';
import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers';
import { SelectedFilters } from './selected_filters';
import { getDefaultConfigs } from '../configurations/default_configs';
-import { NEW_SERIES_KEY } from '../hooks/use_series_storage';
import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames';
describe('SelectedFilters', function () {
mockAppIndexPattern();
const dataViewSeries = getDefaultConfigs({
- reportType: 'pld',
+ reportType: 'dist',
indexPattern: mockIndexPattern,
- seriesId: NEW_SERIES_KEY,
+ dataType: 'ux',
});
it('should render properly', async function () {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
index 6e513fcd2fec9..79218aa111f16 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
@@ -18,6 +18,11 @@ import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
import { SeriesActions } from './columns/series_actions';
import { ChartEditOptions } from './chart_edit_options';
+interface EditItem {
+ seriesConfig: DataSeries;
+ id: string;
+}
+
export function SeriesEditor() {
const { allSeries, firstSeriesId } = useSeriesStorage();
@@ -43,8 +48,8 @@ export function SeriesEditor() {
}),
field: 'defaultFilters',
width: '15%',
- render: (defaultFilters: string[], series: DataSeries) => (
-
+ render: (defaultFilters: string[], { id, seriesConfig }: EditItem) => (
+
),
},
{
@@ -53,8 +58,8 @@ export function SeriesEditor() {
}),
field: 'breakdowns',
width: '25%',
- render: (val: string[], item: DataSeries) => (
-
+ render: (val: string[], item: EditItem) => (
+
),
},
{
@@ -69,7 +74,7 @@ export function SeriesEditor() {
width: '20%',
field: 'id',
align: 'right' as const,
- render: (val: string, item: DataSeries) => ,
+ render: (val: string, item: EditItem) => ,
},
{
name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
@@ -78,7 +83,7 @@ export function SeriesEditor() {
align: 'center' as const,
width: '10%',
field: 'id',
- render: (val: string, item: DataSeries) => ,
+ render: (val: string, item: EditItem) => ,
},
]
: []),
@@ -86,20 +91,21 @@ export function SeriesEditor() {
const allSeriesKeys = Object.keys(allSeries);
- const items: DataSeries[] = [];
+ const items: EditItem[] = [];
const { indexPattern } = useAppIndexPatternContext();
allSeriesKeys.forEach((seriesKey) => {
const series = allSeries[seriesKey];
if (series.reportType && indexPattern) {
- items.push(
- getDefaultConfigs({
+ items.push({
+ id: seriesKey,
+ seriesConfig: getDefaultConfigs({
indexPattern,
reportType: series.reportType,
- seriesId: seriesKey,
- })
- );
+ dataType: series.dataType,
+ }),
+ });
}
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
index 87772532f410d..98605dfdb4ca3 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
@@ -20,17 +20,9 @@ import { IIndexPattern } from '../../../../../../../src/plugins/data/common/inde
import { ExistsFilter } from '../../../../../../../src/plugins/data/common/es_query/filters';
export const ReportViewTypes = {
- pld: 'page-load-dist',
- kpi: 'kpi-trends',
+ dist: 'data-distribution',
+ kpi: 'kpi-over-time',
cwv: 'core-web-vitals',
- upd: 'uptime-duration',
- upp: 'uptime-pings',
- svl: 'service-latency',
- tpt: 'service-throughput',
- logs: 'logs-frequency',
- cpu: 'cpu-usage',
- mem: 'memory-usage',
- nwk: 'network-activity',
} as const;
type ValueOf = T[keyof T];
@@ -60,7 +52,6 @@ export interface ReportDefinition {
export interface DataSeries {
reportType: ReportViewType;
- id: string;
xAxisColumn: Partial | Partial;
yAxisColumns: Array>;
@@ -100,7 +91,6 @@ export interface UrlFilter {
}
export interface ConfigProps {
- seriesId: string;
indexPattern: IIndexPattern;
}
diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx
index f04ca5b857916..9d557a40b7987 100644
--- a/x-pack/plugins/observability/public/components/shared/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/index.tsx
@@ -10,6 +10,7 @@ import type { CoreVitalProps, HeaderMenuPortalProps } from './types';
import type { FieldValueSuggestionsProps } from './field_value_suggestions/types';
export { createLazyObservabilityPageTemplate } from './page_template';
+export type { LazyObservabilityPageTemplateProps } from './page_template';
const CoreVitalsLazy = lazy(() => import('./core_web_vitals/index'));
diff --git a/x-pack/plugins/observability/public/components/shared/page_template/index.ts b/x-pack/plugins/observability/public/components/shared/page_template/index.ts
index 14793ba402251..5ecb2c493f687 100644
--- a/x-pack/plugins/observability/public/components/shared/page_template/index.ts
+++ b/x-pack/plugins/observability/public/components/shared/page_template/index.ts
@@ -6,3 +6,4 @@
*/
export { createLazyObservabilityPageTemplate } from './lazy_page_template';
+export type { LazyObservabilityPageTemplateProps } from './lazy_page_template';
diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
index bebcd53f8ae6c..5a7ce3502ce84 100644
--- a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
+++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
@@ -28,6 +28,7 @@ export type WrappedPageTemplateProps = Pick<
| 'pageContentProps'
| 'pageHeader'
| 'restrictWidth'
+ | 'isEmptyState'
> &
// recreate the exclusivity of bottomBar-related props
ExclusiveUnion<
diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts
index 030046ce7bed9..9c73f1682f819 100644
--- a/x-pack/plugins/observability/public/index.ts
+++ b/x-pack/plugins/observability/public/index.ts
@@ -6,6 +6,7 @@
*/
import { PluginInitializerContext, PluginInitializer } from 'kibana/public';
+import { lazy } from 'react';
import {
Plugin,
ObservabilityPublicPluginsStart,
@@ -43,6 +44,8 @@ export {
FieldValueSuggestions,
} from './components/shared/';
+export type { LazyObservabilityPageTemplateProps } from './components/shared';
+
export {
useTrackPageview,
useUiTracker,
@@ -52,6 +55,7 @@ export {
METRIC_TYPE,
} from './hooks/use_track_metric';
+export const LazyAlertsFlyout = lazy(() => import('./pages/alerts/alerts_flyout'));
export { useFetcher, FETCH_STATUS } from './hooks/use_fetcher';
export * from './typings';
diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx
index 90c75a70c0813..8aae408b1f94b 100644
--- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx
+++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx
@@ -5,14 +5,17 @@
* 2.0.
*/
+import { ALERT_UUID } from '@kbn/rule-data-utils/target/technical_field_names';
import React, { ComponentType } from 'react';
+import type { TopAlertResponse } from '../';
import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public';
import { PluginContext, PluginContextValue } from '../../../context/plugin_context';
-import { TopAlert } from '../';
+import { createObservabilityRuleTypeRegistryMock } from '../../../rules/observability_rule_type_registry_mock';
+import { apmAlertResponseExample } from '../example_data';
import { AlertsFlyout } from './';
interface Args {
- alert: TopAlert;
+ alerts: TopAlertResponse[];
}
export default {
@@ -53,35 +56,18 @@ export default {
],
};
-export function Example({ alert }: Args) {
- return {}} />;
+export function Example({ alerts }: Args) {
+ const selectedAlertId = apmAlertResponseExample[0][ALERT_UUID][0];
+ const observabilityRuleTypeRegistry = createObservabilityRuleTypeRegistryMock();
+ return (
+ {}}
+ />
+ );
}
Example.args = {
- alert: {
- link: '/app/apm/services/opbeans-java?rangeFrom=now-15m&rangeTo=now',
- reason: 'Error count for opbeans-java was above the threshold',
- active: true,
- start: 1618235449493,
- fields: {
- 'rule.id': 'apm.error_rate',
- 'service.environment': ['production'],
- 'service.name': ['opbeans-java'],
- 'rule.name': 'Error count threshold | opbeans-java (smith test)',
- 'kibana.rac.alert.duration.us': 61787000,
- 'kibana.rac.alert.evaluation.threshold': 0,
- 'kibana.rac.alert.status': 'open',
- tags: ['apm', 'service.name:opbeans-java'],
- 'kibana.rac.alert.uuid': 'c50fbc70-0d77-462d-ac0a-f2bd0b8512e4',
- 'rule.uuid': '474920d0-93e9-11eb-ac86-0b455460de81',
- 'event.action': 'active',
- '@timestamp': '2021-04-14T21:43:42.966Z',
- 'kibana.rac.alert.id': 'apm.error_rate_opbeans-java_production',
- 'processor.event': ['error'],
- 'kibana.rac.alert.start': '2021-04-14T21:42:41.179Z',
- 'kibana.rac.producer': 'apm',
- 'event.kind': 'state',
- 'rule.category': 'Error count threshold',
- 'kibana.rac.alert.evaluation.value': 1,
- },
- },
+ alerts: apmAlertResponseExample,
} as Args;
diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx
index b4bf96bcc6905..c7faa28b04685 100644
--- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx
+++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx
@@ -20,28 +20,50 @@ import {
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import moment from 'moment-timezone';
-import React from 'react';
import {
ALERT_DURATION,
ALERT_EVALUATION_THRESHOLD,
ALERT_EVALUATION_VALUE,
ALERT_SEVERITY_LEVEL,
+ ALERT_UUID,
RULE_CATEGORY,
RULE_NAME,
} from '@kbn/rule-data-utils/target/technical_field_names';
-import { TopAlert } from '../';
-import { useUiSetting } from '../../../../../../../src/plugins/kibana_react/public';
+import moment from 'moment-timezone';
+import React, { useMemo } from 'react';
+import type { TopAlertResponse } from '../';
+import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana_react/public';
import { asDuration } from '../../../../common/utils/formatters';
-import { usePluginContext } from '../../../hooks/use_plugin_context';
+import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry';
+import { decorateResponse } from '../decorate_response';
import { SeverityBadge } from '../severity_badge';
-type AlertsFlyoutProps = { alert: TopAlert } & EuiFlyoutProps;
+type AlertsFlyoutProps = {
+ alerts?: TopAlertResponse[];
+ isInApp?: boolean;
+ observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
+ selectedAlertId?: string;
+} & EuiFlyoutProps;
-export function AlertsFlyout({ onClose, alert }: AlertsFlyoutProps) {
+export function AlertsFlyout({
+ alerts,
+ isInApp = false,
+ observabilityRuleTypeRegistry,
+ onClose,
+ selectedAlertId,
+}: AlertsFlyoutProps) {
const dateFormat = useUiSetting('dateFormat');
- const { core } = usePluginContext();
- const { prepend } = core.http.basePath;
+ const { services } = useKibana();
+ const { http } = services;
+ const prepend = http?.basePath.prepend;
+ const decoratedAlerts = useMemo(() => {
+ return decorateResponse(alerts ?? [], observabilityRuleTypeRegistry);
+ }, [alerts, observabilityRuleTypeRegistry]);
+ const alert = decoratedAlerts?.find((a) => a.fields[ALERT_UUID] === selectedAlertId);
+
+ if (!alert) {
+ return null;
+ }
const overviewListItems = [
{
@@ -107,11 +129,11 @@ export function AlertsFlyout({ onClose, alert }: AlertsFlyoutProps) {
listItems={overviewListItems}
/>
- {alert.link && (
+ {alert.link && !isInApp && (
-
+
View in app
@@ -121,3 +143,6 @@ export function AlertsFlyout({ onClose, alert }: AlertsFlyoutProps) {
);
}
+
+// eslint-disable-next-line import/no-default-export
+export default AlertsFlyout;
diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx
index 31e59679854b1..28d211766cfe5 100644
--- a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx
+++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx
@@ -9,133 +9,158 @@ import {
CustomItemAction,
EuiBasicTable,
EuiBasicTableColumn,
- EuiBasicTableProps,
EuiButton,
EuiIconTip,
EuiLink,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import React, { useState } from 'react';
import {
ALERT_DURATION,
ALERT_SEVERITY_LEVEL,
+ ALERT_UUID,
} from '@kbn/rule-data-utils/target/technical_field_names';
+import React, { Suspense, useMemo, useState } from 'react';
+import { LazyAlertsFlyout } from '../..';
import { asDuration } from '../../../common/utils/formatters';
import { TimestampTooltip } from '../../components/shared/timestamp_tooltip';
import { usePluginContext } from '../../hooks/use_plugin_context';
-import type { TopAlert } from './';
-import { AlertsFlyout } from './alerts_flyout';
+import type { TopAlert, TopAlertResponse } from './';
+import { decorateResponse } from './decorate_response';
import { SeverityBadge } from './severity_badge';
-type AlertsTableProps = Omit<
- EuiBasicTableProps,
- 'columns' | 'isSelectable' | 'pagination' | 'selection'
->;
+const pagination = { pageIndex: 0, pageSize: 0, totalItemCount: 0 };
+
+interface AlertsTableProps {
+ items: TopAlertResponse[];
+}
export function AlertsTable(props: AlertsTableProps) {
- const [flyoutAlert, setFlyoutAlert] = useState(undefined);
- const handleFlyoutClose = () => setFlyoutAlert(undefined);
- const { core } = usePluginContext();
+ const [selectedAlertId, setSelectedAlertId] = useState(undefined);
+ const handleFlyoutClose = () => setSelectedAlertId(undefined);
+ const { core, observabilityRuleTypeRegistry } = usePluginContext();
const { prepend } = core.http.basePath;
+ const items = useMemo(() => decorateResponse(props.items, observabilityRuleTypeRegistry), [
+ props.items,
+ observabilityRuleTypeRegistry,
+ ]);
- const actions: Array> = [
- {
- render: (alert) =>
- alert.link ? (
-
- {i18n.translate('xpack.observability.alertsTable.viewInAppButtonLabel', {
- defaultMessage: 'View in app',
- })}
-
- ) : (
- <>>
- ),
- isPrimary: true,
- },
- ];
+ const actions: Array> = useMemo(
+ () => [
+ {
+ render: (alert) =>
+ alert.link ? (
+
+ {i18n.translate('xpack.observability.alertsTable.viewInAppButtonLabel', {
+ defaultMessage: 'View in app',
+ })}
+
+ ) : (
+ <>>
+ ),
+ isPrimary: true,
+ },
+ ],
+ [prepend]
+ );
- const columns: Array> = [
- {
- field: 'active',
- name: i18n.translate('xpack.observability.alertsTable.statusColumnDescription', {
- defaultMessage: 'Status',
- }),
- align: 'center',
- render: (_, alert) => {
- const { active } = alert;
+ const columns: Array> = useMemo(
+ () => [
+ {
+ field: 'active',
+ name: i18n.translate('xpack.observability.alertsTable.statusColumnDescription', {
+ defaultMessage: 'Status',
+ }),
+ align: 'center',
+ render: (_, alert) => {
+ const { active } = alert;
- return active ? (
-
- ) : (
-
- );
+ return active ? (
+
+ ) : (
+
+ );
+ },
+ },
+ {
+ field: 'start',
+ name: i18n.translate('xpack.observability.alertsTable.triggeredColumnDescription', {
+ defaultMessage: 'Triggered',
+ }),
+ render: (_, alert) => {
+ return (
+
+ );
+ },
},
- },
- {
- field: 'start',
- name: i18n.translate('xpack.observability.alertsTable.triggeredColumnDescription', {
- defaultMessage: 'Triggered',
- }),
- render: (_, item) => {
- return ;
+ {
+ field: 'duration',
+ name: i18n.translate('xpack.observability.alertsTable.durationColumnDescription', {
+ defaultMessage: 'Duration',
+ }),
+ render: (_, alert) => {
+ const { active } = alert;
+ return active ? null : asDuration(alert.fields[ALERT_DURATION], { extended: true });
+ },
},
- },
- {
- field: 'duration',
- name: i18n.translate('xpack.observability.alertsTable.durationColumnDescription', {
- defaultMessage: 'Duration',
- }),
- render: (_, alert) => {
- const { active } = alert;
- return active ? null : asDuration(alert.fields[ALERT_DURATION], { extended: true });
+ {
+ field: 'severity',
+ name: i18n.translate('xpack.observability.alertsTable.severityColumnDescription', {
+ defaultMessage: 'Severity',
+ }),
+ render: (_, alert) => {
+ return ;
+ },
},
- },
- {
- field: 'severity',
- name: i18n.translate('xpack.observability.alertsTable.severityColumnDescription', {
- defaultMessage: 'Severity',
- }),
- render: (_, alert) => {
- return ;
+ {
+ field: 'reason',
+ name: i18n.translate('xpack.observability.alertsTable.reasonColumnDescription', {
+ defaultMessage: 'Reason',
+ }),
+ dataType: 'string',
+ render: (_, alert) => {
+ return (
+ setSelectedAlertId(alert.fields[ALERT_UUID])}>
+ {alert.reason}
+
+ );
+ },
},
- },
- {
- field: 'reason',
- name: i18n.translate('xpack.observability.alertsTable.reasonColumnDescription', {
- defaultMessage: 'Reason',
- }),
- dataType: 'string',
- render: (_, item) => {
- return setFlyoutAlert(item)}>{item.reason} ;
+ {
+ actions,
+ name: i18n.translate('xpack.observability.alertsTable.actionsColumnDescription', {
+ defaultMessage: 'Actions',
+ }),
},
- },
- {
- actions,
- name: i18n.translate('xpack.observability.alertsTable.actionsColumnDescription', {
- defaultMessage: 'Actions',
- }),
- },
- ];
+ ],
+ [actions, setSelectedAlertId]
+ );
return (
<>
- {flyoutAlert && }
+
+
+
- {...props}
columns={columns}
+ items={items}
tableLayout="auto"
- pagination={{ pageIndex: 0, pageSize: 0, totalItemCount: 0 }}
+ pagination={pagination}
/>
>
);
diff --git a/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts b/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts
new file mode 100644
index 0000000000000..e177bea6c6dac
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ RULE_ID,
+ RULE_NAME,
+ ALERT_STATUS,
+ ALERT_START,
+} from '@kbn/rule-data-utils/target/technical_field_names';
+import type { TopAlertResponse, TopAlert } from '.';
+import { parseTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields';
+import { asDuration, asPercent } from '../../../common/utils/formatters';
+import { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_rule_type_registry';
+
+export function decorateResponse(
+ alerts: TopAlertResponse[],
+ observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry
+): TopAlert[] {
+ return alerts.map((alert) => {
+ const parsedFields = parseTechnicalFields(alert);
+ const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[RULE_ID]!);
+ const formatted = {
+ link: undefined,
+ reason: parsedFields[RULE_NAME]!,
+ ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}),
+ };
+
+ return {
+ ...formatted,
+ fields: parsedFields,
+ active: parsedFields[ALERT_STATUS] !== 'closed',
+ start: new Date(parsedFields[ALERT_START]!).getTime(),
+ };
+ });
+}
diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/index.tsx
index b76e9f82d8dfe..bd926f3a326bf 100644
--- a/x-pack/plugins/observability/public/pages/alerts/index.tsx
+++ b/x-pack/plugins/observability/public/pages/alerts/index.tsx
@@ -7,21 +7,10 @@
import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import {
- ALERT_START,
- ALERT_STATUS,
- RULE_ID,
- RULE_NAME,
-} from '@kbn/rule-data-utils/target/technical_field_names';
import React from 'react';
import { useHistory } from 'react-router-dom';
-import { format, parse } from 'url';
-import {
- ParsedTechnicalFields,
- parseTechnicalFields,
-} from '../../../../rule_registry/common/parse_technical_fields';
+import { ParsedTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields';
import type { AlertStatus } from '../../../common/typings';
-import { asDuration, asPercent } from '../../../common/utils/formatters';
import { ExperimentalBadge } from '../../components/shared/experimental_badge';
import { useFetcher } from '../../hooks/use_fetcher';
import { usePluginContext } from '../../hooks/use_plugin_context';
@@ -48,7 +37,7 @@ interface AlertsPageProps {
}
export function AlertsPage({ routeParams }: AlertsPageProps) {
- const { core, observabilityRuleTypeRegistry, ObservabilityPageTemplate } = usePluginContext();
+ const { core, ObservabilityPageTemplate } = usePluginContext();
const { prepend } = core.http.basePath;
const history = useHistory();
const {
@@ -61,7 +50,7 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
'/app/management/insightsAndAlerting/triggersActions/alerts'
);
- const { data: topAlerts } = useFetcher(
+ const { data: alerts } = useFetcher(
({ signal }) => {
const { start, end } = getAbsoluteDateRange({ rangeFrom, rangeTo });
@@ -79,38 +68,9 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
status,
},
},
- }).then((alerts) => {
- return alerts.map((alert) => {
- const parsedFields = parseTechnicalFields(alert);
- const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[RULE_ID]!);
- const formatted = {
- link: undefined,
- reason: parsedFields[RULE_NAME]!,
- ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}),
- };
-
- const parsedLink = formatted.link ? parse(formatted.link, true) : undefined;
-
- return {
- ...formatted,
- fields: parsedFields,
- link: parsedLink
- ? format({
- ...parsedLink,
- query: {
- ...parsedLink.query,
- rangeFrom,
- rangeTo,
- },
- })
- : undefined,
- active: parsedFields[ALERT_STATUS] !== 'closed',
- start: new Date(parsedFields[ALERT_START]!).getTime(),
- };
- });
});
},
- [kuery, observabilityRuleTypeRegistry, rangeFrom, rangeTo, status]
+ [kuery, rangeFrom, rangeTo, status]
);
function setStatusFilter(value: AlertStatus) {
@@ -193,7 +153,7 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
-
+
diff --git a/x-pack/plugins/observability/public/rules/observability_rule_type_registry_mock.ts b/x-pack/plugins/observability/public/rules/observability_rule_type_registry_mock.ts
index b2cf48f8e1c32..ad1fc13b8daf2 100644
--- a/x-pack/plugins/observability/public/rules/observability_rule_type_registry_mock.ts
+++ b/x-pack/plugins/observability/public/rules/observability_rule_type_registry_mock.ts
@@ -8,6 +8,7 @@
import { ObservabilityRuleTypeRegistry } from './create_observability_rule_type_registry';
const createRuleTypeRegistryMock = () => ({
+ getFormatter: () => () => 'a reason',
registerFormatter: () => {},
});
diff --git a/x-pack/plugins/osquery/common/types.ts b/x-pack/plugins/osquery/common/types.ts
index 11c418a51fc7c..ab9e36ec335db 100644
--- a/x-pack/plugins/osquery/common/types.ts
+++ b/x-pack/plugins/osquery/common/types.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { PackagePolicy, PackagePolicyInput, PackagePolicyInputStream } from '../../fleet/common';
+
export const savedQuerySavedObjectType = 'osquery-saved-query';
export const packSavedObjectType = 'osquery-pack';
export type SavedObjectType = 'osquery-saved-query' | 'osquery-pack';
@@ -25,3 +27,31 @@ export type RequiredKeepUndefined = { [K in keyof T]-?: [T[K]] } extends infe
? { [K in keyof U]: U[K][0] }
: never
: never;
+
+export interface OsqueryManagerPackagePolicyConfigRecordEntry {
+ type: string;
+ value: string;
+ frozen?: boolean;
+}
+
+export interface OsqueryManagerPackagePolicyConfigRecord {
+ id: OsqueryManagerPackagePolicyConfigRecordEntry;
+ query: OsqueryManagerPackagePolicyConfigRecordEntry;
+ interval: OsqueryManagerPackagePolicyConfigRecordEntry;
+ platform?: OsqueryManagerPackagePolicyConfigRecordEntry;
+ version?: OsqueryManagerPackagePolicyConfigRecordEntry;
+}
+
+export interface OsqueryManagerPackagePolicyInputStream
+ extends Omit {
+ config?: OsqueryManagerPackagePolicyConfigRecord;
+ vars?: OsqueryManagerPackagePolicyConfigRecord;
+}
+
+export interface OsqueryManagerPackagePolicyInput extends Omit {
+ streams: OsqueryManagerPackagePolicyInputStream[];
+}
+
+export interface OsqueryManagerPackagePolicy extends Omit {
+ inputs: OsqueryManagerPackagePolicyInput[];
+}
diff --git a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx
index ffa86c547656c..23277976968a9 100644
--- a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx
+++ b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx
@@ -23,6 +23,7 @@ import {
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
+import { PLUGIN_ID } from '../../../fleet/common';
import { pagePathGetters } from '../../../fleet/public';
import { useActionResults } from './use_action_results';
import { useAllResults } from '../results/use_all_results';
@@ -130,7 +131,7 @@ const ActionResultsSummaryComponent: React.FC = ({
(agentId) => (
= ({ policyId }
const href = useMemo(
() =>
- getUrlForApp('fleet', {
+ getUrlForApp(PLUGIN_ID, {
path: `#` + pagePathGetters.policy_details({ policyId }),
}),
[getUrlForApp, policyId]
@@ -36,7 +37,7 @@ const AgentsPolicyLinkComponent: React.FC = ({ policyId }
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
event.preventDefault();
- return navigateToApp('fleet', {
+ return navigateToApp(PLUGIN_ID, {
path: `#` + pagePathGetters.policy_details({ policyId }),
});
}
diff --git a/x-pack/plugins/osquery/public/components/manage_integration_link.tsx b/x-pack/plugins/osquery/public/components/manage_integration_link.tsx
index 8419003f57715..b28471a907e04 100644
--- a/x-pack/plugins/osquery/public/components/manage_integration_link.tsx
+++ b/x-pack/plugins/osquery/public/components/manage_integration_link.tsx
@@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui';
+import { INTEGRATIONS_PLUGIN_ID } from '../../../fleet/common';
import { pagePathGetters } from '../../../fleet/public';
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
@@ -22,12 +23,12 @@ const ManageIntegrationLinkComponent = () => {
const integrationHref = useMemo(() => {
if (osqueryIntegration) {
- return getUrlForApp('fleet', {
+ return getUrlForApp(INTEGRATIONS_PLUGIN_ID, {
path:
'#' +
pagePathGetters.integration_details_policies({
pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`,
- }),
+ })[1],
});
}
}, [getUrlForApp, osqueryIntegration]);
@@ -37,12 +38,12 @@ const ManageIntegrationLinkComponent = () => {
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
event.preventDefault();
if (osqueryIntegration) {
- return navigateToApp('fleet', {
+ return navigateToApp(INTEGRATIONS_PLUGIN_ID, {
path:
'#' +
pagePathGetters.integration_details_policies({
pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`,
- }),
+ })[1],
});
}
}
diff --git a/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx b/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx
index 808718c55d199..d8169c25ad929 100644
--- a/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx
+++ b/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx
@@ -9,16 +9,17 @@ import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useMemo } from 'react';
+import { PLUGIN_ID } from '../../common';
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
interface NavigationButtonsProps {
isDisabled?: boolean;
- integrationPolicyId?: string;
- agentPolicyId?: string;
+ integrationPolicyId?: string | undefined;
+ agentPolicyId?: string | undefined;
}
const NavigationButtonsComponent: React.FC = ({
- isDisabled,
+ isDisabled = false,
integrationPolicyId,
agentPolicyId,
}) => {
@@ -28,7 +29,7 @@ const NavigationButtonsComponent: React.FC = ({
const liveQueryHref = useMemo(
() =>
- getUrlForApp('osquery', {
+ getUrlForApp(PLUGIN_ID, {
path: agentPolicyId
? `/live_queries/new?agentPolicyId=${agentPolicyId}`
: ' `/live_queries/new',
@@ -40,7 +41,7 @@ const NavigationButtonsComponent: React.FC = ({
(event) => {
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
event.preventDefault();
- navigateToApp('osquery', {
+ navigateToApp(PLUGIN_ID, {
path: agentPolicyId
? `/live_queries/new?agentPolicyId=${agentPolicyId}`
: ' `/live_queries/new',
@@ -50,7 +51,7 @@ const NavigationButtonsComponent: React.FC = ({
[agentPolicyId, navigateToApp]
);
- const scheduleQueryGroupsHref = getUrlForApp('osquery', {
+ const scheduleQueryGroupsHref = getUrlForApp(PLUGIN_ID, {
path: integrationPolicyId
? `/scheduled_query_groups/${integrationPolicyId}/edit`
: `/scheduled_query_groups`,
@@ -60,7 +61,7 @@ const NavigationButtonsComponent: React.FC = ({
(event) => {
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
event.preventDefault();
- navigateToApp('osquery', {
+ navigateToApp(PLUGIN_ID, {
path: integrationPolicyId
? `/scheduled_query_groups/${integrationPolicyId}/edit`
: `/scheduled_query_groups`,
diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx
index 6dfbc086c394a..2305df807f1c8 100644
--- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx
+++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx
@@ -15,8 +15,10 @@ import { i18n } from '@kbn/i18n';
import {
agentRouteService,
agentPolicyRouteService,
- PackagePolicy,
AgentPolicy,
+ PLUGIN_ID,
+ INTEGRATIONS_PLUGIN_ID,
+ NewPackagePolicy,
} from '../../../fleet/common';
import {
pagePathGetters,
@@ -27,6 +29,7 @@ import {
import { ScheduledQueryGroupQueriesTable } from '../scheduled_query_groups/scheduled_query_group_queries_table';
import { useKibana } from '../common/lib/kibana';
import { NavigationButtons } from './navigation_buttons';
+import { OsqueryManagerPackagePolicy } from '../../common/types';
/**
* Exports Osquery-specific package policy instructions
@@ -51,7 +54,7 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
const agentsLinkHref = useMemo(() => {
if (!policy?.policy_id) return '#';
- return getUrlForApp('fleet', {
+ return getUrlForApp(PLUGIN_ID, {
path:
`#` +
pagePathGetters.policy_details({ policyId: policy?.policy_id }) +
@@ -128,13 +131,13 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
replace({
state: {
onSaveNavigateTo: (newPackagePolicy) => [
- 'fleet',
+ INTEGRATIONS_PLUGIN_ID,
{
path:
'#' +
pagePathGetters.integration_policy_edit({
packagePolicyId: newPackagePolicy.id,
- }),
+ })[1],
state: {
forceRefresh: true,
},
@@ -146,7 +149,11 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
}, [editMode, replace]);
const scheduledQueryGroupTableData = useMemo(() => {
- const policyWithoutEmptyQueries = produce(newPolicy, (draft) => {
+ const policyWithoutEmptyQueries = produce<
+ NewPackagePolicy,
+ OsqueryManagerPackagePolicy,
+ OsqueryManagerPackagePolicy
+ >(newPolicy, (draft) => {
draft.inputs[0].streams = filter(['compiled_stream.id', null], draft.inputs[0].streams);
return draft;
});
@@ -205,7 +212,9 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
{editMode && scheduledQueryGroupTableData.inputs[0].streams.length ? (
-
+
) : null}
diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx
index a120354261305..960de043eac6e 100644
--- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx
+++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx
@@ -125,7 +125,7 @@ const ScheduledQueryGroupDetailsPageComponent = () => {
return (
- {data && }
+ {data && }
);
};
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx
deleted file mode 100644
index 3879a375b857c..0000000000000
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import {
- EuiFlyout,
- EuiTitle,
- EuiSpacer,
- EuiFlyoutBody,
- EuiFlyoutHeader,
- EuiFlyoutFooter,
- EuiPortal,
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonEmpty,
- EuiButton,
-} from '@elastic/eui';
-import React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-
-import { CodeEditorField } from '../../queries/form/code_editor_field';
-import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations';
-import { Form, useForm, FormData, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
-
-const FORM_ID = 'addQueryFlyoutForm';
-
-const CommonUseField = getUseField({ component: Field });
-
-interface AddQueryFlyoutProps {
- onSave: (payload: FormData) => Promise;
- onClose: () => void;
-}
-
-const AddQueryFlyoutComponent: React.FC = ({ onSave, onClose }) => {
- const { form } = useForm({
- id: FORM_ID,
- // @ts-expect-error update types
- onSubmit: (payload, isValid) => {
- if (isValid) {
- onSave(payload);
- onClose();
- }
- },
- schema: {
- id: {
- type: FIELD_TYPES.TEXT,
- label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
- defaultMessage: 'ID',
- }),
- validations: idFieldValidations.map((validator) => ({ validator })),
- },
- query: {
- type: FIELD_TYPES.TEXT,
- label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', {
- defaultMessage: 'Query',
- }),
- validations: [{ validator: queryFieldValidation }],
- },
- interval: {
- type: FIELD_TYPES.NUMBER,
- label: i18n.translate(
- 'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.intervalFieldLabel',
- {
- defaultMessage: 'Interval (s)',
- }
- ),
- validations: [{ validator: intervalFieldValidation }],
- },
- },
- });
-
- const { submit } = form;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export const AddQueryFlyout = React.memo(AddQueryFlyoutComponent);
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx
deleted file mode 100644
index f44b5e45a26e5..0000000000000
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import {
- EuiFlyout,
- EuiTitle,
- EuiSpacer,
- EuiFlyoutBody,
- EuiFlyoutHeader,
- EuiFlyoutFooter,
- EuiPortal,
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonEmpty,
- EuiButton,
-} from '@elastic/eui';
-import React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-
-import { PackagePolicyInputStream } from '../../../../fleet/common';
-import { CodeEditorField } from '../../queries/form/code_editor_field';
-import { Form, useForm, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
-import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations';
-
-const FORM_ID = 'editQueryFlyoutForm';
-
-const CommonUseField = getUseField({ component: Field });
-
-interface EditQueryFlyoutProps {
- defaultValue: PackagePolicyInputStream;
- onSave: (payload: FormData) => void;
- onClose: () => void;
-}
-
-export const EditQueryFlyout: React.FC = ({
- defaultValue,
- onSave,
- onClose,
-}) => {
- const { form } = useForm({
- id: FORM_ID,
- // @ts-expect-error update types
- onSubmit: (payload, isValid) => {
- if (isValid) {
- // @ts-expect-error update types
- onSave(payload);
- onClose();
- }
- return;
- },
- defaultValue,
- deserializer: (payload) => ({
- id: payload.vars.id.value,
- query: payload.vars.query.value,
- interval: payload.vars.interval.value,
- }),
- schema: {
- id: {
- type: FIELD_TYPES.TEXT,
- label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
- defaultMessage: 'ID',
- }),
- validations: idFieldValidations.map((validator) => ({ validator })),
- },
- query: {
- type: FIELD_TYPES.TEXT,
- label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', {
- defaultMessage: 'Query',
- }),
- validations: [{ validator: queryFieldValidation }],
- },
- interval: {
- type: FIELD_TYPES.NUMBER,
- label: i18n.translate(
- 'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.intervalFieldLabel',
- {
- defaultMessage: 'Interval (s)',
- }
- ),
- validations: [{ validator: intervalFieldValidation }],
- },
- },
- });
-
- const { submit } = form;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx
index 8924a61d181b6..64efdf61fc735 100644
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx
@@ -24,13 +24,22 @@ import { produce } from 'immer';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
+import { PLUGIN_ID } from '../../../common';
+import { OsqueryManagerPackagePolicy } from '../../../common/types';
import {
AgentPolicy,
- PackagePolicy,
PackagePolicyPackage,
packagePolicyRouteService,
} from '../../../../fleet/common';
-import { Form, useForm, useFormData, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
+import {
+ Form,
+ useForm,
+ useFormData,
+ getUseField,
+ Field,
+ FIELD_TYPES,
+ fieldValidators,
+} from '../../shared_imports';
import { useKibana, useRouterNavigate } from '../../common/lib/kibana';
import { PolicyIdComboBoxField } from './policy_id_combobox_field';
import { QueriesField } from './queries_field';
@@ -44,7 +53,7 @@ const FORM_ID = 'scheduledQueryForm';
const CommonUseField = getUseField({ component: Field });
interface ScheduledQueryGroupFormProps {
- defaultValue?: PackagePolicy;
+ defaultValue?: OsqueryManagerPackagePolicy;
packageInfo?: PackagePolicyPackage;
editMode?: boolean;
}
@@ -89,7 +98,7 @@ const ScheduledQueryGroupFormComponent: React.FC =
{
onSuccess: (data) => {
if (!editMode) {
- navigateToApp('osquery', { path: `scheduled_query_groups/${data.item.id}` });
+ navigateToApp(PLUGIN_ID, { path: `scheduled_query_groups/${data.item.id}` });
toasts.addSuccess(
i18n.translate('xpack.osquery.scheduledQueryGroup.form.createSuccessToastMessageText', {
defaultMessage: 'Successfully scheduled {scheduledQueryGroupName}',
@@ -101,7 +110,7 @@ const ScheduledQueryGroupFormComponent: React.FC =
return;
}
- navigateToApp('osquery', { path: `scheduled_query_groups/${data.item.id}` });
+ navigateToApp(PLUGIN_ID, { path: `scheduled_query_groups/${data.item.id}` });
toasts.addSuccess(
i18n.translate('xpack.osquery.scheduledQueryGroup.form.updateSuccessToastMessageText', {
defaultMessage: 'Successfully updated {scheduledQueryGroupName}',
@@ -118,7 +127,15 @@ const ScheduledQueryGroupFormComponent: React.FC =
}
);
- const { form } = useForm({
+ const { form } = useForm<
+ Omit & {
+ policy_id: string;
+ },
+ Omit & {
+ policy_id: string[];
+ namespace: string[];
+ }
+ >({
id: FORM_ID,
schema: {
name: {
@@ -126,6 +143,18 @@ const ScheduledQueryGroupFormComponent: React.FC =
label: i18n.translate('xpack.osquery.scheduledQueryGroup.form.nameFieldLabel', {
defaultMessage: 'Name',
}),
+ validations: [
+ {
+ validator: fieldValidators.emptyField(
+ i18n.translate(
+ 'xpack.osquery.scheduledQueryGroup.form.nameFieldRequiredErrorMessage',
+ {
+ defaultMessage: 'Name is a required field',
+ }
+ )
+ ),
+ },
+ ],
},
description: {
type: FIELD_TYPES.TEXT,
@@ -144,19 +173,35 @@ const ScheduledQueryGroupFormComponent: React.FC =
label: i18n.translate('xpack.osquery.scheduledQueryGroup.form.agentPolicyFieldLabel', {
defaultMessage: 'Agent policy',
}),
+ validations: [
+ {
+ validator: fieldValidators.emptyField(
+ i18n.translate(
+ 'xpack.osquery.scheduledQueryGroup.form.policyIdFieldRequiredErrorMessage',
+ {
+ defaultMessage: 'Agent policy is a required field',
+ }
+ )
+ ),
+ },
+ ],
},
},
- onSubmit: (payload) => {
+ onSubmit: (payload, isValid) => {
+ if (!isValid) return Promise.resolve();
const formData = produce(payload, (draft) => {
- // @ts-expect-error update types
- draft.inputs[0].streams.forEach((stream) => {
- delete stream.compiled_stream;
+ if (draft.inputs?.length) {
+ draft.inputs[0].streams?.forEach((stream) => {
+ delete stream.compiled_stream;
+
+ // we don't want to send id as null when creating the policy
+ if (stream.id == null) {
+ // @ts-expect-error update types
+ delete stream.id;
+ }
+ });
+ }
- // we don't want to send id as null when creating the policy
- if (stream.id == null) {
- delete stream.id;
- }
- });
return draft;
});
return mutateAsync(formData);
@@ -164,7 +209,6 @@ const ScheduledQueryGroupFormComponent: React.FC =
options: {
stripEmptyFields: false,
},
- // @ts-expect-error update types
deserializer: (payload) => ({
...payload,
policy_id: payload.policy_id.length ? [payload.policy_id] : [],
@@ -172,9 +216,7 @@ const ScheduledQueryGroupFormComponent: React.FC =
}),
serializer: (payload) => ({
...payload,
- // @ts-expect-error update types
policy_id: payload.policy_id[0],
- // @ts-expect-error update types
namespace: payload.namespace[0],
}),
defaultValue: merge(
@@ -182,10 +224,11 @@ const ScheduledQueryGroupFormComponent: React.FC =
name: '',
description: '',
enabled: true,
- policy_id: [],
+ policy_id: '',
namespace: 'default',
output_id: '',
- package: packageInfo,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ package: packageInfo!,
inputs: [
{
type: 'osquery',
@@ -205,7 +248,15 @@ const ScheduledQueryGroupFormComponent: React.FC =
[defaultValue, agentPolicyOptions]
);
- const [{ policy_id: policyId }] = useFormData({ form, watch: ['policy_id'] });
+ const [
+ {
+ package: { version: integrationPackageVersion } = { version: undefined },
+ policy_id: policyId,
+ },
+ ] = useFormData({
+ form,
+ watch: ['package', 'policy_id'],
+ });
const currentPolicy = useMemo(() => {
if (!policyId) {
@@ -288,6 +339,7 @@ const ScheduledQueryGroupFormComponent: React.FC =
path="inputs"
component={QueriesField}
scheduledQueryGroupId={defaultValue?.id ?? null}
+ integrationPackageVersion={integrationPackageVersion}
/>
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx
index 34c6eaea1c265..0718ff028e002 100644
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx
@@ -11,16 +11,20 @@ import { produce } from 'immer';
import React, { useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { PackagePolicyInput, PackagePolicyInputStream } from '../../../../fleet/common';
+import {
+ OsqueryManagerPackagePolicyInputStream,
+ OsqueryManagerPackagePolicyInput,
+} from '../../../common/types';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { FieldHook } from '../../shared_imports';
import { ScheduledQueryGroupQueriesTable } from '../scheduled_query_group_queries_table';
-import { AddQueryFlyout } from './add_query_flyout';
-import { EditQueryFlyout } from './edit_query_flyout';
+import { QueryFlyout } from '../queries/query_flyout';
import { OsqueryPackUploader } from './pack_uploader';
+import { getSupportedPlatforms } from '../queries/platforms/helpers';
interface QueriesFieldProps {
- field: FieldHook;
+ field: FieldHook;
+ integrationPackageVersion?: string | undefined;
scheduledQueryGroupId: string;
}
@@ -28,29 +32,53 @@ interface GetNewStreamProps {
id: string;
interval: string;
query: string;
+ platform?: string | undefined;
+ version?: string | undefined;
scheduledQueryGroupId?: string;
}
-const getNewStream = ({ id, interval, query, scheduledQueryGroupId }: GetNewStreamProps) => ({
- data_stream: { type: 'logs', dataset: `${OSQUERY_INTEGRATION_NAME}.result` },
- enabled: true,
- id: scheduledQueryGroupId
- ? `osquery-${OSQUERY_INTEGRATION_NAME}.result-${scheduledQueryGroupId}`
- : null,
- vars: {
- id: { type: 'text', value: id },
- interval: {
- type: 'integer',
- value: interval,
+interface GetNewStreamReturn extends Omit {
+ id?: string | null;
+}
+
+const getNewStream = (payload: GetNewStreamProps) =>
+ produce(
+ {
+ data_stream: { type: 'logs', dataset: `${OSQUERY_INTEGRATION_NAME}.result` },
+ enabled: true,
+ id: payload.scheduledQueryGroupId
+ ? `osquery-${OSQUERY_INTEGRATION_NAME}.result-${payload.scheduledQueryGroupId}`
+ : null,
+ vars: {
+ id: { type: 'text', value: payload.id },
+ interval: {
+ type: 'integer',
+ value: payload.interval,
+ },
+ query: { type: 'text', value: payload.query },
+ },
},
- query: { type: 'text', value: query },
- },
-});
+ (draft) => {
+ if (payload.platform && draft.vars) {
+ draft.vars.platform = { type: 'text', value: payload.platform };
+ }
+ if (payload.version && draft.vars) {
+ draft.vars.version = { type: 'text', value: payload.version };
+ }
+ return draft;
+ }
+ );
-const QueriesFieldComponent: React.FC = ({ field, scheduledQueryGroupId }) => {
+const QueriesFieldComponent: React.FC = ({
+ field,
+ integrationPackageVersion,
+ scheduledQueryGroupId,
+}) => {
const [showAddQueryFlyout, setShowAddQueryFlyout] = useState(false);
const [showEditQueryFlyout, setShowEditQueryFlyout] = useState(-1);
- const [tableSelectedItems, setTableSelectedItems] = useState([]);
+ const [tableSelectedItems, setTableSelectedItems] = useState<
+ OsqueryManagerPackagePolicyInputStream[]
+ >([]);
const handleShowAddFlyout = useCallback(() => setShowAddQueryFlyout(true), []);
const handleHideAddFlyout = useCallback(() => setShowAddQueryFlyout(false), []);
@@ -59,7 +87,7 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
const { setValue } = field;
const handleDeleteClick = useCallback(
- (stream: PackagePolicyInputStream) => {
+ (stream: OsqueryManagerPackagePolicyInputStream) => {
const streamIndex = findIndex(field.value[0].streams, [
'vars.id.value',
stream.vars?.id.value,
@@ -79,7 +107,7 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
);
const handleEditClick = useCallback(
- (stream: PackagePolicyInputStream) => {
+ (stream: OsqueryManagerPackagePolicyInputStream) => {
const streamIndex = findIndex(field.value[0].streams, [
'vars.id.value',
stream.vars?.id.value,
@@ -91,39 +119,61 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
);
const handleEditQuery = useCallback(
- (updatedQuery) => {
- if (showEditQueryFlyout >= 0) {
- setValue(
- produce((draft) => {
- draft[0].streams[showEditQueryFlyout].vars.id.value = updatedQuery.id;
- draft[0].streams[showEditQueryFlyout].vars.interval.value = updatedQuery.interval;
- draft[0].streams[showEditQueryFlyout].vars.query.value = updatedQuery.query;
+ (updatedQuery) =>
+ new Promise((resolve) => {
+ if (showEditQueryFlyout >= 0) {
+ setValue(
+ produce((draft) => {
+ draft[0].streams[showEditQueryFlyout].vars.id.value = updatedQuery.id;
+ draft[0].streams[showEditQueryFlyout].vars.interval.value = updatedQuery.interval;
+ draft[0].streams[showEditQueryFlyout].vars.query.value = updatedQuery.query;
- return draft;
- })
- );
- }
+ if (updatedQuery.platform?.length) {
+ draft[0].streams[showEditQueryFlyout].vars.platform = {
+ type: 'text',
+ value: updatedQuery.platform,
+ };
+ } else {
+ delete draft[0].streams[showEditQueryFlyout].vars.platform;
+ }
- handleHideEditFlyout();
- },
+ if (updatedQuery.version?.length) {
+ draft[0].streams[showEditQueryFlyout].vars.version = {
+ type: 'text',
+ value: updatedQuery.version,
+ };
+ } else {
+ delete draft[0].streams[showEditQueryFlyout].vars.version;
+ }
+
+ return draft;
+ })
+ );
+ }
+
+ handleHideEditFlyout();
+ resolve();
+ }),
[handleHideEditFlyout, setValue, showEditQueryFlyout]
);
const handleAddQuery = useCallback(
- (newQuery) => {
- setValue(
- produce((draft) => {
- draft[0].streams.push(
- getNewStream({
- ...newQuery,
- scheduledQueryGroupId,
- })
- );
- return draft;
- })
- );
- handleHideAddFlyout();
- },
+ (newQuery) =>
+ new Promise((resolve) => {
+ setValue(
+ produce((draft) => {
+ draft[0].streams.push(
+ getNewStream({
+ ...newQuery,
+ scheduledQueryGroupId,
+ })
+ );
+ return draft;
+ })
+ );
+ handleHideAddFlyout();
+ resolve();
+ }),
[handleHideAddFlyout, scheduledQueryGroupId, setValue]
);
@@ -148,6 +198,8 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
id: newQueryId,
interval: newQuery.interval,
query: newQuery.query,
+ version: newQuery.version,
+ platform: getSupportedPlatforms(newQuery.platform),
scheduledQueryGroupId,
})
);
@@ -160,7 +212,9 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
[scheduledQueryGroupId, setValue]
);
- const tableData = useMemo(() => ({ inputs: field.value }), [field.value]);
+ const tableData = useMemo(() => (field.value.length ? field.value[0].streams : []), [
+ field.value,
+ ]);
return (
<>
@@ -201,12 +255,16 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
{ }
{showAddQueryFlyout && (
- // @ts-expect-error update types
-
+
)}
{showEditQueryFlyout != null && showEditQueryFlyout >= 0 && (
-
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts
new file mode 100644
index 0000000000000..3345c18d07b2c
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts
@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const ALL_OSQUERY_VERSIONS_OPTIONS = [
+ {
+ label: '4.7.0',
+ },
+ {
+ label: '4.6.0',
+ },
+ {
+ label: '4.5.1',
+ },
+ {
+ label: '4.5.0',
+ },
+ {
+ label: '4.4.0',
+ },
+ {
+ label: '4.3.0',
+ },
+ {
+ label: '4.2.0',
+ },
+ {
+ label: '4.1.2',
+ },
+ {
+ label: '4.1.1',
+ },
+ {
+ label: '4.0.2',
+ },
+ {
+ label: '3.3.2',
+ },
+ {
+ label: '3.3.0',
+ },
+ {
+ label: '3.2.6',
+ },
+ {
+ label: '3.2.4',
+ },
+ {
+ label: '2.9.0',
+ },
+ {
+ label: '2.8.0',
+ },
+ {
+ label: '2.7.0',
+ },
+ {
+ label: '2.11.2',
+ },
+ {
+ label: '2.11.0',
+ },
+ {
+ label: '2.10.2',
+ },
+ {
+ label: '2.10.0',
+ },
+];
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platform_checkbox_group_field.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platform_checkbox_group_field.tsx
new file mode 100644
index 0000000000000..4e433e9e240b1
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platform_checkbox_group_field.tsx
@@ -0,0 +1,134 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { isEmpty, pickBy } from 'lodash';
+import React, { useCallback, useMemo, useState } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+ EuiCheckboxGroup,
+ EuiCheckboxGroupOption,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { FieldHook, getFieldValidityAndErrorMessage } from '../../shared_imports';
+import { PlatformIcon } from './platforms/platform_icon';
+
+interface Props {
+ field: FieldHook;
+ euiFieldProps?: Record;
+ idAria?: string;
+ [key: string]: unknown;
+}
+
+export const PlatformCheckBoxGroupField = ({
+ field,
+ euiFieldProps = {},
+ idAria,
+ ...rest
+}: Props) => {
+ const options = useMemo(
+ () => [
+ {
+ id: 'linux',
+ label: (
+
+
+
+
+
+
+
+
+ ),
+ },
+ {
+ id: 'darwin',
+ label: (
+
+
+
+
+
+
+
+
+ ),
+ },
+ {
+ id: 'windows',
+ label: (
+
+
+
+
+
+
+
+
+ ),
+ },
+ ],
+ []
+ );
+
+ const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
+ const [checkboxIdToSelectedMap, setCheckboxIdToSelectedMap] = useState>(
+ () =>
+ (options as EuiCheckboxGroupOption[]).reduce((acc, option) => {
+ acc[option.id] = isEmpty(field.value) ? true : field.value?.includes(option.id) ?? false;
+ return acc;
+ }, {} as Record)
+ );
+
+ const onChange = useCallback(
+ (optionId: string) => {
+ const newCheckboxIdToSelectedMap = {
+ ...checkboxIdToSelectedMap,
+ [optionId]: !checkboxIdToSelectedMap[optionId],
+ };
+ setCheckboxIdToSelectedMap(newCheckboxIdToSelectedMap);
+
+ field.setValue(() =>
+ Object.keys(pickBy(newCheckboxIdToSelectedMap, (value) => value === true)).join(',')
+ );
+ },
+ [checkboxIdToSelectedMap, field]
+ );
+
+ const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]);
+
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/constants.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/constants.ts
new file mode 100644
index 0000000000000..4f81ed73e1e7a
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/constants.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { PlatformType } from './types';
+
+export const SUPPORTED_PLATFORMS = [PlatformType.darwin, PlatformType.linux, PlatformType.windows];
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/helpers.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/helpers.tsx
new file mode 100644
index 0000000000000..362fa5c67e6f9
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/helpers.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { uniq } from 'lodash';
+import { SUPPORTED_PLATFORMS } from './constants';
+
+import linuxSvg from './logos/linux.svg';
+import windowsSvg from './logos/windows.svg';
+import macosSvg from './logos/macos.svg';
+import { PlatformType } from './types';
+
+export const getPlatformIconModule = (platform: string) => {
+ switch (platform) {
+ case 'darwin':
+ return macosSvg;
+ case 'linux':
+ return linuxSvg;
+ case 'windows':
+ return windowsSvg;
+ default:
+ return `${platform}`;
+ }
+};
+
+export const getSupportedPlatforms = (payload: string) => {
+ let platformArray: string[];
+ try {
+ platformArray = payload?.split(',').map((platformString) => platformString.trim());
+ } catch (e) {
+ return undefined;
+ }
+
+ if (!platformArray) return;
+
+ return uniq(
+ platformArray.reduce((acc, nextPlatform) => {
+ if (!SUPPORTED_PLATFORMS.includes(nextPlatform as PlatformType)) {
+ if (nextPlatform === 'posix') {
+ acc.push(PlatformType.darwin);
+ acc.push(PlatformType.linux);
+ }
+ if (nextPlatform === 'ubuntu') {
+ acc.push(PlatformType.linux);
+ }
+ } else {
+ acc.push(nextPlatform);
+ }
+ return acc;
+ }, [] as string[])
+ ).join(',');
+};
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/index.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/index.tsx
new file mode 100644
index 0000000000000..b8af2790c6f36
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/index.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import React, { useEffect, useState, useMemo } from 'react';
+
+import { SUPPORTED_PLATFORMS } from './constants';
+import { PlatformIcon } from './platform_icon';
+
+interface PlatformIconsProps {
+ platform: string;
+}
+
+const PlatformIconsComponent: React.FC = ({ platform }) => {
+ const [platforms, setPlatforms] = useState(SUPPORTED_PLATFORMS);
+
+ useEffect(() => {
+ setPlatforms((prevValue) => {
+ if (platform) {
+ let platformArray: string[];
+ try {
+ platformArray = platform?.split(',').map((platformString) => platformString.trim());
+ } catch (e) {
+ return prevValue;
+ }
+ return platformArray;
+ } else {
+ return SUPPORTED_PLATFORMS;
+ }
+ });
+ }, [platform]);
+
+ const content = useMemo(
+ () =>
+ platforms.map((platformString) => (
+
+
+
+ )),
+ [platforms]
+ );
+
+ return {content} ;
+};
+
+export const PlatformIcons = React.memo(PlatformIconsComponent);
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/linux.svg b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/linux.svg
new file mode 100644
index 0000000000000..47358292e08a8
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/linux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/macos.svg b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/macos.svg
new file mode 100644
index 0000000000000..baa5930800aa9
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/macos.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/windows.svg b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/windows.svg
new file mode 100644
index 0000000000000..0872225da3a11
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/windows.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/platform_icon.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/platform_icon.tsx
new file mode 100644
index 0000000000000..1126dfd690c19
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/platform_icon.tsx
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiIcon } from '@elastic/eui';
+import React from 'react';
+import { getPlatformIconModule } from './helpers';
+
+interface PlatformIconProps {
+ platform: string;
+}
+
+const PlatformIconComponent: React.FC = ({ platform }) => {
+ const platformIconModule = getPlatformIconModule(platform);
+ return ;
+};
+
+export const PlatformIcon = React.memo(PlatformIconComponent);
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/types.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/types.ts
new file mode 100644
index 0000000000000..94953a6a854ea
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/types.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export enum PlatformType {
+ darwin = 'darwin',
+ windows = 'windows',
+ linux = 'linux',
+}
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx
new file mode 100644
index 0000000000000..62ac3a46a2d77
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx
@@ -0,0 +1,176 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ EuiCallOut,
+ EuiFlyout,
+ EuiTitle,
+ EuiSpacer,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiFlyoutFooter,
+ EuiPortal,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiButton,
+} from '@elastic/eui';
+import React, { useMemo } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { satisfies } from 'semver';
+
+import { OsqueryManagerPackagePolicyConfigRecord } from '../../../common/types';
+import { CodeEditorField } from '../../queries/form/code_editor_field';
+import { Form, getUseField, Field } from '../../shared_imports';
+import { PlatformCheckBoxGroupField } from './platform_checkbox_group_field';
+import { ALL_OSQUERY_VERSIONS_OPTIONS } from './constants';
+import {
+ UseScheduledQueryGroupQueryFormProps,
+ useScheduledQueryGroupQueryForm,
+} from './use_scheduled_query_group_query_form';
+import { ManageIntegrationLink } from '../../components/manage_integration_link';
+
+const CommonUseField = getUseField({ component: Field });
+
+interface QueryFlyoutProps {
+ defaultValue?: UseScheduledQueryGroupQueryFormProps['defaultValue'] | undefined;
+ integrationPackageVersion?: string | undefined;
+ onSave: (payload: OsqueryManagerPackagePolicyConfigRecord) => Promise;
+ onClose: () => void;
+}
+
+const QueryFlyoutComponent: React.FC = ({
+ defaultValue,
+ integrationPackageVersion,
+ onSave,
+ onClose,
+}) => {
+ const { form } = useScheduledQueryGroupQueryForm({
+ defaultValue,
+ handleSubmit: (payload, isValid) =>
+ new Promise((resolve) => {
+ if (isValid) {
+ onSave(payload);
+ onClose();
+ }
+ resolve();
+ }),
+ });
+
+ /* Platform and version fields are supported since osquer_manger@0.3.0 */
+ const isFieldSupported = useMemo(
+ () => (integrationPackageVersion ? satisfies(integrationPackageVersion, '>=0.3.0') : false),
+ [integrationPackageVersion]
+ );
+
+ const { submit } = form;
+
+ return (
+
+
+
+
+
+ {defaultValue ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {!isFieldSupported ? (
+
+ }
+ iconType="pin"
+ >
+
+
+
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const QueryFlyout = React.memo(QueryFlyoutComponent);
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx
new file mode 100644
index 0000000000000..344c33b419dd6
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { FIELD_TYPES } from '../../shared_imports';
+
+import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations';
+
+export const formSchema = {
+ id: {
+ type: FIELD_TYPES.TEXT,
+ label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
+ defaultMessage: 'ID',
+ }),
+ validations: idFieldValidations.map((validator) => ({ validator })),
+ },
+ query: {
+ type: FIELD_TYPES.TEXT,
+ label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', {
+ defaultMessage: 'Query',
+ }),
+ validations: [{ validator: queryFieldValidation }],
+ },
+ interval: {
+ defaultValue: 3600,
+ type: FIELD_TYPES.NUMBER,
+ label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.intervalFieldLabel', {
+ defaultMessage: 'Interval (s)',
+ }),
+ validations: [{ validator: intervalFieldValidation }],
+ },
+ platform: {
+ type: FIELD_TYPES.TEXT,
+ label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.platformFieldLabel', {
+ defaultMessage: 'Platform',
+ }),
+ validations: [],
+ },
+ version: {
+ defaultValue: [],
+ type: FIELD_TYPES.COMBO_BOX,
+ label: ((
+
+
+
+
+
+
+
+
+
+
+ ) as unknown) as string,
+ validations: [],
+ },
+};
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx
new file mode 100644
index 0000000000000..bcde5f4b970d4
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { isArray } from 'lodash';
+import uuid from 'uuid';
+import { produce } from 'immer';
+
+import { FormConfig, useForm } from '../../shared_imports';
+import { OsqueryManagerPackagePolicyConfigRecord } from '../../../common/types';
+import { formSchema } from './schema';
+
+const FORM_ID = 'editQueryFlyoutForm';
+
+export interface UseScheduledQueryGroupQueryFormProps {
+ defaultValue?: OsqueryManagerPackagePolicyConfigRecord | undefined;
+ handleSubmit: FormConfig<
+ OsqueryManagerPackagePolicyConfigRecord,
+ ScheduledQueryGroupFormData
+ >['onSubmit'];
+}
+
+export interface ScheduledQueryGroupFormData {
+ id: string;
+ query: string;
+ interval: number;
+ platform?: string | undefined;
+ version?: string[] | undefined;
+}
+
+export const useScheduledQueryGroupQueryForm = ({
+ defaultValue,
+ handleSubmit,
+}: UseScheduledQueryGroupQueryFormProps) =>
+ useForm({
+ id: FORM_ID + uuid.v4(),
+ onSubmit: handleSubmit,
+ options: {
+ stripEmptyFields: false,
+ },
+ defaultValue,
+ // @ts-expect-error update types
+ serializer: (payload) =>
+ produce(payload, (draft) => {
+ if (draft.platform?.split(',').length === 3) {
+ // if all platforms are checked then use undefined
+ delete draft.platform;
+ }
+ if (isArray(draft.version)) {
+ if (!draft.version.length) {
+ delete draft.version;
+ } else {
+ // @ts-expect-error update types
+ draft.version = draft.version[0];
+ }
+ }
+ return draft;
+ }),
+ deserializer: (payload) => {
+ if (!payload) return {} as ScheduledQueryGroupFormData;
+
+ return {
+ id: payload.id.value,
+ query: payload.query.value,
+ interval: parseInt(payload.interval.value, 10),
+ platform: payload.platform?.value,
+ version: payload.version?.value ? [payload.version?.value] : [],
+ };
+ },
+ schema: formSchema,
+ });
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/validations.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/validations.ts
similarity index 100%
rename from x-pack/plugins/osquery/public/scheduled_query_groups/form/validations.ts
rename to x-pack/plugins/osquery/public/scheduled_query_groups/queries/validations.ts
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx
index 6f78f2c086edf..36d15587086f2 100644
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx
@@ -22,9 +22,10 @@ import {
PersistedIndexPatternLayer,
PieVisualizationState,
} from '../../../lens/public';
-import { PackagePolicy, PackagePolicyInputStream } from '../../../fleet/common';
import { FilterStateStore } from '../../../../../src/plugins/data/common';
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
+import { PlatformIcons } from './queries/platforms';
+import { OsqueryManagerPackagePolicyInputStream } from '../../common/types';
export enum ViewResultsActionButtonType {
icon = 'icon',
@@ -303,12 +304,12 @@ const ViewResultsInDiscoverActionComponent: React.FC;
+ data: OsqueryManagerPackagePolicyInputStream[];
editMode?: boolean;
- onDeleteClick?: (item: PackagePolicyInputStream) => void;
- onEditClick?: (item: PackagePolicyInputStream) => void;
- selectedItems?: PackagePolicyInputStream[];
- setSelectedItems?: (selection: PackagePolicyInputStream[]) => void;
+ onDeleteClick?: (item: OsqueryManagerPackagePolicyInputStream) => void;
+ onEditClick?: (item: OsqueryManagerPackagePolicyInputStream) => void;
+ selectedItems?: OsqueryManagerPackagePolicyInputStream[];
+ setSelectedItems?: (selection: OsqueryManagerPackagePolicyInputStream[]) => void;
}
const ScheduledQueryGroupQueriesTableComponent: React.FC = ({
@@ -320,7 +321,7 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC {
const renderDeleteAction = useCallback(
- (item: PackagePolicyInputStream) => (
+ (item: OsqueryManagerPackagePolicyInputStream) => (
(
+ (item: OsqueryManagerPackagePolicyInputStream) => (
,
+ []
+ );
+
+ const renderVersionColumn = useCallback(
+ (version: string) =>
+ version
+ ? `${version}`
+ : i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel', {
+ defaultMessage: 'ALL',
+ }),
+ []
+ );
+
const renderDiscoverResultsAction = useCallback(
(item) => (
({
sort: {
- field: 'vars.id.value' as keyof PackagePolicyInputStream,
+ field: 'vars.id.value' as keyof OsqueryManagerPackagePolicyInputStream,
direction: 'asc' as const,
},
}),
[]
);
- const itemId = useCallback((item: PackagePolicyInputStream) => get('vars.id.value', item), []);
+ const itemId = useCallback(
+ (item: OsqueryManagerPackagePolicyInputStream) => get('vars.id.value', item),
+ []
+ );
const selection = useMemo(
() => ({
@@ -477,8 +512,8 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC
- items={data.inputs[0].streams}
+
+ items={data}
itemId={itemId}
columns={columns}
sorting={sorting}
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts
index e0f892d0302c0..93d552b3f71f3 100644
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts
@@ -8,11 +8,8 @@
import { useQuery } from 'react-query';
import { useKibana } from '../common/lib/kibana';
-import {
- GetOnePackagePolicyResponse,
- PackagePolicy,
- packagePolicyRouteService,
-} from '../../../fleet/common';
+import { GetOnePackagePolicyResponse, packagePolicyRouteService } from '../../../fleet/common';
+import { OsqueryManagerPackagePolicy } from '../../common/types';
interface UseScheduledQueryGroup {
scheduledQueryGroupId: string;
@@ -25,7 +22,11 @@ export const useScheduledQueryGroup = ({
}: UseScheduledQueryGroup) => {
const { http } = useKibana().services;
- return useQuery(
+ return useQuery<
+ Omit & { item: OsqueryManagerPackagePolicy },
+ unknown,
+ OsqueryManagerPackagePolicy
+ >(
['scheduledQueryGroup', { scheduledQueryGroupId }],
() => http.get(packagePolicyRouteService.getInfoPath(scheduledQueryGroupId)),
{
diff --git a/x-pack/plugins/osquery/public/shared_imports.ts b/x-pack/plugins/osquery/public/shared_imports.ts
index 737b4d4735777..8a569a0761656 100644
--- a/x-pack/plugins/osquery/public/shared_imports.ts
+++ b/x-pack/plugins/osquery/public/shared_imports.ts
@@ -12,6 +12,7 @@ export {
FieldValidateResponse,
FIELD_TYPES,
Form,
+ FormConfig,
FormData,
FormDataProvider,
FormHook,
diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json
index 291b0f7c607cf..76e26c770cfe0 100644
--- a/x-pack/plugins/osquery/tsconfig.json
+++ b/x-pack/plugins/osquery/tsconfig.json
@@ -12,7 +12,8 @@
"common/**/*",
"public/**/*",
"scripts/**/*",
- "server/**/*"
+ "server/**/*",
+ "../../../typings/**/*"
],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js
index c91732019f79f..209c224618f78 100644
--- a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js
+++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js
@@ -181,6 +181,10 @@ describe(' ', () => {
expect(exists('remoteClusterCreateButton')).toBe(true);
});
+ test('should have link to documentation', () => {
+ expect(exists('documentationLink')).toBe(true);
+ });
+
test('should list the remote clusters in the table', () => {
expect(tableCellsValues.length).toEqual(remoteClusters.length);
expect(tableCellsValues).toEqual([
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js
index 1706be8cfbe2f..2b2e7338fb6b0 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js
@@ -5,62 +5,38 @@
* 2.0.
*/
-import React, { Fragment } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
import { remoteClustersUrl } from '../../../services/documentation';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiText,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiPageHeader, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
export const RemoteClusterPageTitle = ({ title, description }) => (
-
-
-
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
-
-
-
- {description ? (
- <>
-
-
-
- {description}
-
- >
- ) : null}
-
-
-
+ <>
+ {title}}
+ rightSideItems={[
+
+
+ ,
+ ]}
+ description={description}
+ />
+
+
+ >
);
RemoteClusterPageTitle.propTypes = {
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js
index 124d2d42afb78..f62550ca5aa10 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js
@@ -9,8 +9,6 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageContent } from '@elastic/eui';
-
import { extractQueryParams } from '../../../shared_imports';
import { getRouter, redirect } from '../../services';
import { setBreadcrumbs } from '../../services/breadcrumb';
@@ -58,11 +56,7 @@ export class RemoteClusterAdd extends PureComponent {
const { isAddingCluster, addClusterError } = this.props;
return (
-
+ <>
-
+ >
);
}
}
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js
index 18ee2e2b3875d..1f3388d06e54c 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js
@@ -5,27 +5,17 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiButtonEmpty,
- EuiCallOut,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
- EuiPageContent,
- EuiSpacer,
- EuiText,
- EuiTextColor,
-} from '@elastic/eui';
+import { EuiButton, EuiCallOut, EuiEmptyPrompt, EuiPageContent, EuiSpacer } from '@elastic/eui';
import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { getRouter, redirect } from '../../services';
import { setBreadcrumbs } from '../../services/breadcrumb';
-import { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components';
+import { RemoteClusterPageTitle, RemoteClusterForm } from '../components';
export class RemoteClusterEdit extends Component {
static propTypes = {
@@ -92,56 +82,50 @@ export class RemoteClusterEdit extends Component {
}
};
- renderContent() {
+ render() {
const { clusterName } = this.state;
const { isLoading, cluster, isEditingCluster, getEditClusterError } = this.props;
if (isLoading) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
}
if (!cluster) {
return (
-
-
+
+
+
+
}
- color="danger"
- iconType="alert"
- >
-
-
-
-
-
-
+
+
+ }
+ actions={
+
@@ -149,10 +133,10 @@ export class RemoteClusterEdit extends Component {
id="xpack.remoteClusters.edit.viewRemoteClustersButtonLabel"
defaultMessage="View remote clusters"
/>
-
-
-
-
+
+ }
+ />
+
);
}
@@ -160,23 +144,50 @@ export class RemoteClusterEdit extends Component {
if (isConfiguredByNode) {
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+ }
+ body={
+
+
+
+ }
+ actions={
+
+
+
+ }
+ />
+
);
}
return (
<>
+
+ }
+ />
+
{hasDeprecatedProxySetting ? (
<>
>
) : null}
+
);
}
-
- render() {
- return (
-
-
- }
- />
-
- {this.renderContent()}
-
- );
- }
}
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js
index ccf4c7568f7ad..b94ae8f7edbc0 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js
@@ -5,32 +5,24 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
+ EuiButtonEmpty,
EuiEmptyPrompt,
- EuiFlexGroup,
- EuiFlexItem,
EuiLoadingKibana,
- EuiLoadingSpinner,
EuiOverlayMask,
- EuiPageBody,
EuiPageContent,
- EuiPageContentHeader,
- EuiPageContentHeaderSection,
EuiSpacer,
- EuiText,
- EuiTextColor,
- EuiTitle,
- EuiCallOut,
+ EuiPageHeader,
} from '@elastic/eui';
+import { remoteClustersUrl } from '../../services/documentation';
import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { setBreadcrumbs } from '../../services/breadcrumb';
import { RemoteClusterTable } from './remote_cluster_table';
@@ -82,41 +74,6 @@ export class RemoteClusterList extends Component {
clearInterval(this.interval);
}
- getHeaderSection(isAuthorized) {
- return (
-
-
-
-
-
-
-
-
-
-
- {isAuthorized && (
-
-
-
-
-
- )}
-
-
-
- );
- }
-
renderBlockingAction() {
const { isCopyingCluster, isRemovingCluster } = this.props;
@@ -132,16 +89,28 @@ export class RemoteClusterList extends Component {
}
renderNoPermission() {
- const title = i18n.translate('xpack.remoteClusters.remoteClusterList.noPermissionTitle', {
- defaultMessage: 'Permission error',
- });
return (
-
-
+
+
+
+ }
+ body={
+
+
+
+ }
/>
-
+
);
}
@@ -150,80 +119,84 @@ export class RemoteClusterList extends Component {
// handle unexpected error shapes in the API action.
const { statusCode, error: errorString } = error.body;
- const title = i18n.translate('xpack.remoteClusters.remoteClusterList.loadingErrorTitle', {
- defaultMessage: 'Error loading remote clusters',
- });
return (
-
- {statusCode} {errorString}
-
+
+
+
+
+ }
+ body={
+
+ {statusCode} {errorString}
+
+ }
+ />
+
);
}
renderEmpty() {
return (
-
-
-
- }
- body={
-
+
+
+
+
+ }
+ body={
-
- }
- actions={
-
-
-
- }
- />
+ }
+ actions={
+
+
+
+ }
+ />
+
);
}
renderLoading() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
);
}
@@ -231,10 +204,35 @@ export class RemoteClusterList extends Component {
const { clusters } = this.props;
return (
-
+ <>
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
-
+ >
);
}
@@ -242,7 +240,6 @@ export class RemoteClusterList extends Component {
const { isLoading, clusters, clusterLoadError } = this.props;
const isEmpty = !isLoading && !clusters.length;
const isAuthorized = !clusterLoadError || clusterLoadError.status !== 403;
- const isHeaderVisible = clusterLoadError || !isEmpty;
let content;
@@ -261,13 +258,10 @@ export class RemoteClusterList extends Component {
}
return (
-
-
- {isHeaderVisible && this.getHeaderSection(isAuthorized)}
- {content}
- {this.renderBlockingAction()}
-
-
+ <>
+ {content}
+ {this.renderBlockingAction()}
+ >
);
}
}
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js
index 3da8bb505fc54..1404e51d98a6d 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js
@@ -330,6 +330,19 @@ export class RemoteClusterTable extends Component {
)}
) : undefined,
+ toolsRight: (
+
+
+
+ ),
onChange: this.onSearch,
box: {
incremental: true,
diff --git a/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js b/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js
index 343237e70a120..1a6459627c9a1 100644
--- a/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js
+++ b/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js
@@ -5,9 +5,7 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
-
-import { loadClusters as sendLoadClustersRequest, showApiError } from '../../services';
+import { loadClusters as sendLoadClustersRequest } from '../../services';
import { LOAD_CLUSTERS_START, LOAD_CLUSTERS_SUCCESS, LOAD_CLUSTERS_FAILURE } from '../action_types';
@@ -20,17 +18,10 @@ export const loadClusters = () => async (dispatch) => {
try {
clusters = await sendLoadClustersRequest();
} catch (error) {
- dispatch({
+ return dispatch({
type: LOAD_CLUSTERS_FAILURE,
payload: { error },
});
-
- return showApiError(
- error,
- i18n.translate('xpack.remoteClusters.loadAction.errorTitle', {
- defaultMessage: 'Error loading remote clusters',
- })
- );
}
dispatch({
diff --git a/x-pack/plugins/remote_clusters/public/shared_imports.ts b/x-pack/plugins/remote_clusters/public/shared_imports.ts
index fd28175318666..c8d7f1d9f13f3 100644
--- a/x-pack/plugins/remote_clusters/public/shared_imports.ts
+++ b/x-pack/plugins/remote_clusters/public/shared_imports.ts
@@ -5,4 +5,8 @@
* 2.0.
*/
-export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public';
+export {
+ extractQueryParams,
+ indices,
+ SectionLoading,
+} from '../../../../src/plugins/es_ui_shared/public';
diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx
index 6442c81a47184..618c91fba0715 100644
--- a/x-pack/plugins/reporting/public/components/report_listing.tsx
+++ b/x-pack/plugins/reporting/public/components/report_listing.tsx
@@ -9,11 +9,10 @@ import {
EuiBasicTable,
EuiFlexGroup,
EuiFlexItem,
- EuiPageContent,
+ EuiPageHeader,
EuiSpacer,
EuiText,
EuiTextColor,
- EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
@@ -139,38 +138,29 @@ class ReportListingUi extends Component {
public render() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {this.renderTable()}
-
+ <>
+
+ }
+ description={
+
+ }
+ />
+
+
+ {this.renderTable()}
+
-
+ >
);
}
diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json
index f6e7b8bf46a39..a29c01b0f31cc 100644
--- a/x-pack/plugins/security/kibana.json
+++ b/x-pack/plugins/security/kibana.json
@@ -1,5 +1,10 @@
{
"id": "security",
+ "owner": {
+ "name": "Platform Security",
+ "githubTeam": "kibana-security"
+ },
+ "description": "This plugin provides authentication and authorization features, and exposes functionality to understand the capabilities of the currently authenticated user.",
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "security"],
diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
index 04190fbf5eacd..b1bc14cc79e64 100644
--- a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
+++ b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: expected value of type [boolean] but got [string]"`;
+exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [2]: expected value of type [boolean] but got [string]"`;
exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected actions"`;
diff --git a/x-pack/plugins/security/server/authorization/validate_es_response.ts b/x-pack/plugins/security/server/authorization/validate_es_response.ts
index d941b2d777c4f..223aee37a9047 100644
--- a/x-pack/plugins/security/server/authorization/validate_es_response.ts
+++ b/x-pack/plugins/security/server/authorization/validate_es_response.ts
@@ -9,6 +9,11 @@ import { schema } from '@kbn/config-schema';
import type { HasPrivilegesResponse } from './types';
+const anyBoolean = schema.boolean();
+const anyBooleanArray = schema.arrayOf(anyBoolean);
+const anyString = schema.string();
+const anyObject = schema.object({}, { unknowns: 'allow' });
+
/**
* Validates an Elasticsearch "Has privileges" response against the expected application, actions, and resources.
*
@@ -31,7 +36,6 @@ export function validateEsPrivilegeResponse(
}
function buildValidationSchema(application: string, actions: string[], resources: string[]) {
- const actionValidationSchema = schema.boolean();
const actionsValidationSchema = schema.object(
{},
{
@@ -45,9 +49,7 @@ function buildValidationSchema(application: string, actions: string[], resources
throw new Error('Payload did not match expected actions');
}
- Object.values(value).forEach((actionResult) => {
- actionValidationSchema.validate(actionResult);
- });
+ anyBooleanArray.validate(Object.values(value));
},
}
);
@@ -73,12 +75,12 @@ function buildValidationSchema(application: string, actions: string[], resources
);
return schema.object({
- username: schema.string(),
- has_all_requested: schema.boolean(),
- cluster: schema.object({}, { unknowns: 'allow' }),
+ username: anyString,
+ has_all_requested: anyBoolean,
+ cluster: anyObject,
application: schema.object({
[application]: resourcesValidationSchema,
}),
- index: schema.object({}, { unknowns: 'allow' }),
+ index: anyObject,
});
}
diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts
index 3423f17e3f683..10452996eae6f 100644
--- a/x-pack/plugins/security_solution/common/cti/constants.ts
+++ b/x-pack/plugins/security_solution/common/cti/constants.ts
@@ -44,3 +44,16 @@ export const SORTED_THREAT_SUMMARY_FIELDS = [
INDICATOR_FIRSTSEEN,
INDICATOR_LASTSEEN,
];
+
+export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = {
+ 'file.hash.md5': 'threatintel.indicator.file.hash.md5',
+ 'file.hash.sha1': 'threatintel.indicator.file.hash.sha1',
+ 'file.hash.sha256': 'threatintel.indicator.file.hash.sha256',
+ 'file.pe.imphash': 'threatintel.indicator.file.pe.imphash',
+ 'file.elf.telfhash': 'threatintel.indicator.file.elf.telfhash',
+ 'file.hash.ssdeep': 'threatintel.indicator.file.hash.ssdeep',
+ 'source.ip': 'threatintel.indicator.ip',
+ 'destination.ip': 'threatintel.indicator.ip',
+ 'url.full': 'threatintel.indicator.url.full',
+ 'registry.path': 'threatintel.indicator.registry.path',
+};
diff --git a/x-pack/plugins/security_solution/common/ecs/threat/index.ts b/x-pack/plugins/security_solution/common/ecs/threat/index.ts
index 19923a82dc846..e5e7964c5d09d 100644
--- a/x-pack/plugins/security_solution/common/ecs/threat/index.ts
+++ b/x-pack/plugins/security_solution/common/ecs/threat/index.ts
@@ -10,6 +10,8 @@ import { EventEcs } from '../event';
interface ThreatMatchEcs {
atomic?: string[];
field?: string[];
+ id?: string[];
+ index?: string[];
type?: string[];
}
diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts
index 35c976fbdfb1d..1f3d4307197f8 100644
--- a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts
@@ -48,9 +48,14 @@ export class BaseDataGenerator {
return new Date(now - this.randomChoice(DAY_OFFSETS)).toISOString();
}
- /** Generate either `true` or `false` */
- protected randomBoolean(): boolean {
- return this.random() < 0.5;
+ /**
+ * Generate either `true` or `false`. By default, the boolean is calculated by determining if a
+ * float is less than `0.5`, but that can be adjusted via the input argument
+ *
+ * @param isLessThan
+ */
+ protected randomBoolean(isLessThan: number = 0.5): boolean {
+ return this.random() < isLessThan;
}
/** generate random OS family value */
diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts
index af799de782f48..6cc5ab7f08447 100644
--- a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts
@@ -13,7 +13,7 @@ import { EndpointAction, EndpointActionResponse, ISOLATION_ACTIONS } from '../ty
const ISOLATION_COMMANDS: ISOLATION_ACTIONS[] = ['isolate', 'unisolate'];
export class FleetActionGenerator extends BaseDataGenerator {
- /** Generate an Action */
+ /** Generate a random endpoint Action (isolate or unisolate) */
generate(overrides: DeepPartial = {}): EndpointAction {
const timeStamp = new Date(this.randomPastDate());
@@ -35,6 +35,14 @@ export class FleetActionGenerator extends BaseDataGenerator {
);
}
+ generateIsolateAction(overrides: DeepPartial = {}): EndpointAction {
+ return merge(this.generate({ data: { command: 'isolate' } }), overrides);
+ }
+
+ generateUnIsolateAction(overrides: DeepPartial = {}): EndpointAction {
+ return merge(this.generate({ data: { command: 'unisolate' } }), overrides);
+ }
+
/** Generates an action response */
generateResponse(overrides: DeepPartial = {}): EndpointActionResponse {
const timeStamp = new Date();
@@ -56,6 +64,14 @@ export class FleetActionGenerator extends BaseDataGenerator {
);
}
+ randomFloat(): number {
+ return this.random();
+ }
+
+ randomN(max: number): number {
+ return super.randomN(max);
+ }
+
protected randomIsolateCommand() {
return this.randomChoice(ISOLATION_COMMANDS);
}
diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
index 7e03d9b61fc10..b08d5649540db 100644
--- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
@@ -422,6 +422,14 @@ export class EndpointDocGenerator extends BaseDataGenerator {
this.commonInfo.Endpoint.policy.applied.status = this.randomChoice(POLICY_RESPONSE_STATUSES);
}
+ /**
+ * Update the common host metadata - essentially creating an entire new endpoint metadata record
+ * when the `.generateHostMetadata()` is subsequently called
+ */
+ public updateCommonInfo() {
+ this.commonInfo = this.createHostData();
+ }
+
/**
* Parses an index and returns the data stream fields extracted from the index.
*
@@ -439,7 +447,7 @@ export class EndpointDocGenerator extends BaseDataGenerator {
private createHostData(): HostInfo {
const hostName = this.randomHostname();
- const isIsolated = this.randomBoolean();
+ const isIsolated = this.randomBoolean(0.3);
return {
agent: {
diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts
index 4996d90288ca9..959db0d964aae 100644
--- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts
@@ -13,6 +13,8 @@ import { AxiosResponse } from 'axios';
import { EndpointDocGenerator, Event, TreeOptions } from './generate_data';
import { firstNonNullValue } from './models/ecs_safety_helpers';
import {
+ AGENT_ACTIONS_INDEX,
+ AGENT_ACTIONS_RESULTS_INDEX,
AGENT_POLICY_API_ROUTES,
CreateAgentPolicyRequest,
CreateAgentPolicyResponse,
@@ -25,7 +27,7 @@ import {
PACKAGE_POLICY_API_ROUTES,
} from '../../../fleet/common';
import { policyFactory as policyConfigFactory } from './models/policy_config';
-import { HostMetadata } from './types';
+import { EndpointAction, HostMetadata } from './types';
import { KbnClientWithApiKeySupport } from '../../scripts/endpoint/kbn_client_with_api_key_support';
import { FleetAgentGenerator } from './data_generators/fleet_agent_generator';
import { FleetActionGenerator } from './data_generators/fleet_action_generator';
@@ -409,36 +411,97 @@ const indexFleetActionsForHost = async (
): Promise => {
const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } };
const agentId = endpointHost.elastic.agent.id;
+ const total = fleetActionGenerator.randomN(5);
- for (let i = 0; i < 5; i++) {
+ for (let i = 0; i < total; i++) {
// create an action
- const isolateAction = fleetActionGenerator.generate({
+ const action = fleetActionGenerator.generate({
data: { comment: 'data generator: this host is bad' },
});
- isolateAction.agents = [agentId];
+ action.agents = [agentId];
await esClient.index(
{
- index: '.fleet-actions',
- body: isolateAction,
+ index: AGENT_ACTIONS_INDEX,
+ body: action,
},
ES_INDEX_OPTIONS
);
// Create an action response for the above
- const unIsolateAction = fleetActionGenerator.generateResponse({
- action_id: isolateAction.action_id,
+ const actionResponse = fleetActionGenerator.generateResponse({
+ action_id: action.action_id,
agent_id: agentId,
- action_data: isolateAction.data,
+ action_data: action.data,
});
await esClient.index(
{
- index: '.fleet-actions-results',
- body: unIsolateAction,
+ index: AGENT_ACTIONS_RESULTS_INDEX,
+ body: actionResponse,
},
ES_INDEX_OPTIONS
);
}
+
+ // Add edge cases (maybe)
+ if (fleetActionGenerator.randomFloat() < 0.3) {
+ const randomFloat = fleetActionGenerator.randomFloat();
+
+ // 60% of the time just add either an Isoalte -OR- an UnIsolate action
+ if (randomFloat < 0.6) {
+ let action: EndpointAction;
+
+ if (randomFloat < 0.3) {
+ // add a pending isolation
+ action = fleetActionGenerator.generateIsolateAction({
+ '@timestamp': new Date().toISOString(),
+ });
+ } else {
+ // add a pending UN-isolation
+ action = fleetActionGenerator.generateUnIsolateAction({
+ '@timestamp': new Date().toISOString(),
+ });
+ }
+
+ action.agents = [agentId];
+
+ await esClient.index(
+ {
+ index: AGENT_ACTIONS_INDEX,
+ body: action,
+ },
+ ES_INDEX_OPTIONS
+ );
+ } else {
+ // Else (40% of the time) add a pending isolate AND pending un-isolate
+ const action1 = fleetActionGenerator.generateIsolateAction({
+ '@timestamp': new Date().toISOString(),
+ });
+ const action2 = fleetActionGenerator.generateUnIsolateAction({
+ '@timestamp': new Date().toISOString(),
+ });
+
+ action1.agents = [agentId];
+ action2.agents = [agentId];
+
+ await Promise.all([
+ esClient.index(
+ {
+ index: AGENT_ACTIONS_INDEX,
+ body: action1,
+ },
+ ES_INDEX_OPTIONS
+ ),
+ esClient.index(
+ {
+ index: AGENT_ACTIONS_INDEX,
+ body: action2,
+ },
+ ES_INDEX_OPTIONS
+ ),
+ ]);
+ }
+ }
};
diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts
index 09776b57ed8ea..f58dd1f3370d4 100644
--- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { schema } from '@kbn/config-schema';
+import { schema, TypeOf } from '@kbn/config-schema';
export const HostIsolationRequestSchema = {
body: schema.object({
@@ -22,13 +22,18 @@ export const HostIsolationRequestSchema = {
};
export const EndpointActionLogRequestSchema = {
- // TODO improve when using pagination with query params
- query: schema.object({}),
+ query: schema.object({
+ page: schema.number({ defaultValue: 1, min: 1 }),
+ page_size: schema.number({ defaultValue: 10, min: 1, max: 100 }),
+ }),
params: schema.object({
agent_id: schema.string(),
}),
};
+export type EndpointActionLogRequestParams = TypeOf;
+export type EndpointActionLogRequestQuery = TypeOf;
+
export const ActionStatusRequestSchema = {
query: schema.object({
agent_ids: schema.oneOf([
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
index 937025f35dadc..99753242e7627 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
@@ -38,6 +38,32 @@ export interface EndpointActionResponse {
action_data: EndpointActionData;
}
+export interface ActivityLogAction {
+ type: 'action';
+ item: {
+ // document _id
+ id: string;
+ // document _source
+ data: EndpointAction;
+ };
+}
+export interface ActivityLogActionResponse {
+ type: 'response';
+ item: {
+ // document id
+ id: string;
+ // document _source
+ data: EndpointActionResponse;
+ };
+}
+export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse;
+export interface ActivityLog {
+ total: number;
+ page: number;
+ pageSize: number;
+ data: ActivityLogEntry[];
+}
+
export type HostIsolationRequestBody = TypeOf;
export interface HostIsolationResponse {
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.mock.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.mock.ts
new file mode 100644
index 0000000000000..f3dee5a21e4c9
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.mock.ts
@@ -0,0 +1,110 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IEsSearchResponse } from 'src/plugins/data/public';
+
+import {
+ CtiEventEnrichmentRequestOptions,
+ CtiEventEnrichmentStrategyResponse,
+ CtiQueries,
+} from '.';
+
+export const buildEventEnrichmentRequestOptionsMock = (
+ overrides: Partial = {}
+): CtiEventEnrichmentRequestOptions => ({
+ defaultIndex: ['filebeat-*'],
+ eventFields: {
+ 'file.hash.md5': '1eee2bf3f56d8abed72da2bc523e7431',
+ 'source.ip': '127.0.0.1',
+ 'url.full': 'elastic.co',
+ },
+ factoryQueryType: CtiQueries.eventEnrichment,
+ filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}',
+ timerange: { interval: '', from: '2020-09-13T09:00:43.249Z', to: '2020-09-14T09:00:43.249Z' },
+ ...overrides,
+});
+
+export const buildEventEnrichmentRawResponseMock = (): IEsSearchResponse => ({
+ rawResponse: {
+ took: 17,
+ timed_out: false,
+ _shards: {
+ total: 1,
+ successful: 1,
+ skipped: 0,
+ failed: 0,
+ },
+ hits: {
+ total: {
+ value: 1,
+ relation: 'eq',
+ },
+ max_score: 6.0637846,
+ hits: [
+ {
+ _index: 'filebeat-8.0.0-2021.05.28-000001',
+ _id: '31408415b6d5601a92d29b86c2519658f210c194057588ae396d55cc20b3f03d',
+ _score: 6.0637846,
+ fields: {
+ 'event.category': ['threat'],
+ 'threatintel.indicator.file.type': ['html'],
+ 'related.hash': [
+ '5529de7b60601aeb36f57824ed0e1ae8',
+ '15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e',
+ '768:NXSFGJ/ooP6FawrB7Bo1MWnF/jRmhJImp:1SFXIqBo1Mwj2p',
+ ],
+ 'threatintel.indicator.first_seen': ['2021-05-28T18:33:29.000Z'],
+ 'threatintel.indicator.file.hash.tlsh': [
+ 'FFB20B82F6617061C32784E2712F7A46B179B04FD1EA54A0F28CD8E9CFE4CAA1617F1C',
+ ],
+ 'service.type': ['threatintel'],
+ 'threatintel.indicator.file.hash.ssdeep': [
+ '768:NXSFGJ/ooP6FawrB7Bo1MWnF/jRmhJImp:1SFXIqBo1Mwj2p',
+ ],
+ 'agent.type': ['filebeat'],
+ 'event.module': ['threatintel'],
+ 'threatintel.indicator.type': ['file'],
+ 'agent.name': ['rylastic.local'],
+ 'threatintel.indicator.file.hash.sha256': [
+ '15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e',
+ ],
+ 'event.kind': ['enrichment'],
+ 'threatintel.indicator.file.hash.md5': ['5529de7b60601aeb36f57824ed0e1ae8'],
+ 'fileset.name': ['abusemalware'],
+ 'input.type': ['httpjson'],
+ 'agent.hostname': ['rylastic.local'],
+ tags: ['threatintel-abusemalware', 'forwarded'],
+ 'event.ingested': ['2021-05-28T18:33:55.086Z'],
+ '@timestamp': ['2021-05-28T18:33:52.993Z'],
+ 'agent.id': ['ff93aee5-86a1-4a61-b0e6-0cdc313d01b5'],
+ 'ecs.version': ['1.6.0'],
+ 'event.reference': [
+ 'https://urlhaus-api.abuse.ch/v1/download/15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e/',
+ ],
+ 'event.type': ['indicator'],
+ 'event.created': ['2021-05-28T18:33:52.993Z'],
+ 'agent.ephemeral_id': ['d6b14f65-5bf3-430d-8315-7b5613685979'],
+ 'threatintel.indicator.file.size': [24738],
+ 'agent.version': ['8.0.0'],
+ 'event.dataset': ['threatintel.abusemalware'],
+ },
+ matched_queries: ['file.hash.md5'],
+ },
+ ],
+ },
+ },
+});
+
+export const buildEventEnrichmentResponseMock = (
+ overrides: Partial = {}
+): CtiEventEnrichmentStrategyResponse => ({
+ ...buildEventEnrichmentRawResponseMock(),
+ enrichments: [],
+ inspect: { dsl: ['{"mocked": "json"}'] },
+ totalCount: 0,
+ ...overrides,
+});
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts
new file mode 100644
index 0000000000000..788a44bc5b9f7
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IEsSearchResponse } from 'src/plugins/data/public';
+import { Inspect } from '../../common';
+import { RequestBasicOptions } from '..';
+
+export enum CtiQueries {
+ eventEnrichment = 'eventEnrichment',
+}
+
+export interface CtiEventEnrichmentRequestOptions extends RequestBasicOptions {
+ eventFields: Record;
+}
+
+export type CtiEnrichment = Record;
+
+export interface CtiEventEnrichmentStrategyResponse extends IEsSearchResponse {
+ enrichments: CtiEnrichment[];
+ inspect?: Inspect;
+ totalCount: number;
+}
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
index 956b785079d8d..06d4a16699b8f 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
@@ -66,6 +66,11 @@ import {
MatrixHistogramStrategyResponse,
} from './matrix_histogram';
import { TimerangeInput, SortField, PaginationInput, PaginationInputPaginated } from '../common';
+import {
+ CtiEventEnrichmentRequestOptions,
+ CtiEventEnrichmentStrategyResponse,
+ CtiQueries,
+} from './cti';
export * from './hosts';
export * from './matrix_histogram';
@@ -76,6 +81,7 @@ export type FactoryQueryTypes =
| HostsKpiQueries
| NetworkQueries
| NetworkKpiQueries
+ | CtiQueries
| typeof MatrixHistogramQuery
| typeof MatrixHistogramQueryEntities;
@@ -145,6 +151,8 @@ export type StrategyResponseType = T extends HostsQ
? NetworkKpiUniquePrivateIpsStrategyResponse
: T extends typeof MatrixHistogramQuery
? MatrixHistogramStrategyResponse
+ : T extends CtiQueries.eventEnrichment
+ ? CtiEventEnrichmentStrategyResponse
: never;
export type StrategyRequestType = T extends HostsQueries.hosts
@@ -193,6 +201,8 @@ export type StrategyRequestType = T extends HostsQu
? NetworkKpiUniquePrivateIpsRequestOptions
: T extends typeof MatrixHistogramQuery
? MatrixHistogramRequestOptions
+ : T extends CtiQueries.eventEnrichment
+ ? CtiEventEnrichmentRequestOptions
: never;
export interface DocValueFieldsInput {
diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx
index 740437646f61a..2fdb7e99d860e 100644
--- a/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx
@@ -8,14 +8,10 @@
import React from 'react';
import { FormattedDate, FormattedTime, FormattedRelative } from '@kbn/i18n/react';
-export const FormattedDateAndTime: React.FC<{ date: Date; showRelativeTime?: boolean }> = ({
- date,
- showRelativeTime = false,
-}) => {
+export const FormattedDateAndTime: React.FC<{ date: Date }> = ({ date }) => {
// If date is greater than or equal to 1h (ago), then show it as a date
- // and if showRelativeTime is false
// else, show it as relative to "now"
- return Date.now() - date.getTime() >= 3.6e6 && !showRelativeTime ? (
+ return Date.now() - date.getTime() >= 3.6e6 ? (
<>
{' @'}
diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx
new file mode 100644
index 0000000000000..44405748b6373
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx
@@ -0,0 +1,56 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import {
+ EndpointHostIsolationStatus,
+ EndpointHostIsolationStatusProps,
+} from './endpoint_host_isolation_status';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../../mock/endpoint';
+
+describe('when using the EndpointHostIsolationStatus component', () => {
+ let render: (
+ renderProps?: Partial
+ ) => ReturnType;
+
+ beforeEach(() => {
+ const appContext = createAppRootMockRenderer();
+ render = (renderProps = {}) =>
+ appContext.render(
+
+ );
+ });
+
+ it('should render `null` if not isolated and nothing is pending', () => {
+ const renderResult = render();
+ expect(renderResult.container.textContent).toBe('');
+ });
+
+ it('should show `Isolated` when no pending actions and isolated', () => {
+ const { getByTestId } = render({ isIsolated: true });
+ expect(getByTestId('test').textContent).toBe('Isolated');
+ });
+
+ it.each([
+ ['Isolating pending', { pendingIsolate: 2 }],
+ ['Unisolating pending', { pendingUnIsolate: 2 }],
+ ['4 actions pending', { isIsolated: true, pendingUnIsolate: 2, pendingIsolate: 2 }],
+ ])('should show %s}', (expectedLabel, componentProps) => {
+ const { getByTestId } = render(componentProps);
+ expect(getByTestId('test').textContent).toBe(expectedLabel);
+ // Validate that the text color is set to `subdued`
+ expect(getByTestId('test-pending').classList.contains('euiTextColor--subdued')).toBe(true);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx
index 5cde22de69738..0fe3a8e4337cb 100644
--- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx
@@ -6,8 +6,9 @@
*/
import React, { memo, useMemo } from 'react';
-import { EuiBadge, EuiTextColor } from '@elastic/eui';
+import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiTextColor, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { useTestIdGenerator } from '../../../../management/components/hooks/use_test_id_generator';
export interface EndpointHostIsolationStatusProps {
isIsolated: boolean;
@@ -15,6 +16,7 @@ export interface EndpointHostIsolationStatusProps {
pendingIsolate?: number;
/** the count of pending unisoalte actions */
pendingUnIsolate?: number;
+ 'data-test-subj'?: string;
}
/**
@@ -23,7 +25,9 @@ export interface EndpointHostIsolationStatusProps {
* (`null` is returned)
*/
export const EndpointHostIsolationStatus = memo(
- ({ isIsolated, pendingIsolate = 0, pendingUnIsolate = 0 }) => {
+ ({ isIsolated, pendingIsolate = 0, pendingUnIsolate = 0, 'data-test-subj': dataTestSubj }) => {
+ const getTestId = useTestIdGenerator(dataTestSubj);
+
return useMemo(() => {
// If nothing is pending and host is not currently isolated, then render nothing
if (!isIsolated && !pendingIsolate && !pendingUnIsolate) {
@@ -33,7 +37,7 @@ export const EndpointHostIsolationStatus = memo
+
+
+
+
+
+
+
+
+
+ {pendingIsolate}
+
+
+
+
+
+ {pendingUnIsolate}
+
+
+ }
+ >
+
+
+
+
+
+ );
+ }
// Show 'pending [un]isolate' depending on what's pending
return (
-
-
+
+
{pendingIsolate ? (
);
- }, [isIsolated, pendingIsolate, pendingUnIsolate]);
+ }, [dataTestSubj, getTestId, isIsolated, pendingIsolate, pendingUnIsolate]);
}
);
diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx
index 896b0ec5fd8df..d8895490d1e0f 100644
--- a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx
@@ -13,15 +13,6 @@ import * as i18n from './translations';
const LINE_CLAMP = 3;
const LINE_CLAMP_HEIGHT = 5.5;
-const StyledLineClamp = styled.div`
- display: -webkit-box;
- -webkit-line-clamp: ${LINE_CLAMP};
- -webkit-box-orient: vertical;
- overflow: hidden;
- max-height: ${`${LINE_CLAMP_HEIGHT}em`};
- height: ${`${LINE_CLAMP_HEIGHT}em`};
-`;
-
const ReadMore = styled(EuiButtonEmpty)`
span.euiButtonContent {
padding: 0;
@@ -35,7 +26,19 @@ const ExpandedContent = styled.div`
overflow-y: auto;
`;
-const LineClampComponent: React.FC<{ content?: string | null }> = ({ content }) => {
+const StyledLineClamp = styled.div<{ lineClampHeight: number }>`
+ display: -webkit-box;
+ -webkit-line-clamp: ${LINE_CLAMP};
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ max-height: ${({ lineClampHeight }) => lineClampHeight}em;
+ height: ${({ lineClampHeight }) => lineClampHeight}em;
+`;
+
+const LineClampComponent: React.FC<{
+ content?: string | null;
+ lineClampHeight?: number;
+}> = ({ content, lineClampHeight = LINE_CLAMP_HEIGHT }) => {
const [isOverflow, setIsOverflow] = useState(null);
const [isExpanded, setIsExpanded] = useState(null);
const descriptionRef = useRef(null);
@@ -71,7 +74,11 @@ const LineClampComponent: React.FC<{ content?: string | null }> = ({ content })
{content}
) : isOverflow == null || isOverflow === true ? (
-
+
{content}
) : (
diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx
index 8dac6234f19a8..e7199f6df2b1f 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx
@@ -17,6 +17,7 @@ export const mlModules: string[] = [
'siem_packetbeat',
'siem_winlogbeat',
'siem_winlogbeat_auth',
+ 'security_auth',
'security_linux',
'security_network',
'security_windows',
diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/endpoint_pending_actions.test.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/endpoint_pending_actions.test.ts
new file mode 100644
index 0000000000000..a90f9a3508cd8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/endpoint_pending_actions.test.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { KibanaServices } from '../kibana';
+import { coreMock } from '../../../../../../../src/core/public/mocks';
+import { fetchPendingActionsByAgentId } from './endpoint_pending_actions';
+import { pendingActionsHttpMock, pendingActionsResponseMock } from './mocks';
+import { ACTION_STATUS_ROUTE } from '../../../../common/endpoint/constants';
+
+jest.mock('../kibana');
+
+describe('when using endpoint pending actions api service', () => {
+ let coreHttp: ReturnType['http'];
+
+ beforeEach(() => {
+ const coreStartMock = coreMock.createStart();
+ coreHttp = coreStartMock.http;
+ pendingActionsHttpMock(coreHttp);
+ (KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock);
+ });
+
+ it('should call the endpont pending action status API', async () => {
+ const agentIdList = ['111-111', '222-222'];
+ const response = await fetchPendingActionsByAgentId(agentIdList);
+
+ expect(response).toEqual(pendingActionsResponseMock());
+ expect(coreHttp.get).toHaveBeenCalledWith(ACTION_STATUS_ROUTE, {
+ query: {
+ agent_ids: agentIdList,
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/mocks.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/mocks.ts
new file mode 100644
index 0000000000000..4c3822b07d88c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/mocks.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ PendingActionsRequestQuery,
+ PendingActionsResponse,
+} from '../../../../common/endpoint/types';
+import {
+ httpHandlerMockFactory,
+ ResponseProvidersInterface,
+} from '../../mock/endpoint/http_handler_mock_factory';
+import { ACTION_STATUS_ROUTE } from '../../../../common/endpoint/constants';
+
+export const pendingActionsResponseMock = (): PendingActionsResponse => ({
+ data: [
+ {
+ agent_id: '111-111',
+ pending_actions: {},
+ },
+ {
+ agent_id: '222-222',
+ pending_actions: {
+ isolate: 1,
+ },
+ },
+ ],
+});
+
+export type PendingActionsHttpMockInterface = ResponseProvidersInterface<{
+ pendingActions: () => PendingActionsResponse;
+}>;
+
+export const pendingActionsHttpMock = httpHandlerMockFactory([
+ {
+ id: 'pendingActions',
+ method: 'get',
+ path: ACTION_STATUS_ROUTE,
+ /** Will build a response based on the number of agent ids received. */
+ handler: (options) => {
+ const agentIds = (options.query as PendingActionsRequestQuery).agent_ids as string[];
+
+ if (agentIds.length) {
+ return {
+ data: agentIds.map((id, index) => ({
+ agent_id: id,
+ pending_actions:
+ index % 2 // index's of the array that are not divisible by 2 will will have `isolate: 1`
+ ? {
+ isolate: 1,
+ }
+ : {},
+ })),
+ };
+ }
+
+ return pendingActionsResponseMock();
+ },
+ },
+]);
diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/http_handler_mock_factory.ts b/x-pack/plugins/security_solution/public/common/mock/endpoint/http_handler_mock_factory.ts
index 2df16fc1e21b0..dc93ea8168a3f 100644
--- a/x-pack/plugins/security_solution/public/common/mock/endpoint/http_handler_mock_factory.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/http_handler_mock_factory.ts
@@ -7,12 +7,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import type {
- HttpFetchOptions,
- HttpFetchOptionsWithPath,
- HttpHandler,
- HttpStart,
-} from 'kibana/public';
+import type { HttpFetchOptions, HttpFetchOptionsWithPath, HttpStart } from 'kibana/public';
import { merge } from 'lodash';
import { act } from '@testing-library/react';
@@ -102,7 +97,7 @@ interface RouteMock) => any;
+ handler: (options: HttpFetchOptionsWithPath) => any;
/**
* A function that returns a promise. The API response will be delayed until this promise is
* resolved. This can be helpful when wanting to test an intermediate UI state while the API
@@ -203,14 +198,25 @@ export const httpHandlerMockFactory = pathMatchesPattern(handler.path, path));
if (routeMock) {
- markApiCallAsHandled(responseProvider[routeMock.id].mockDelay);
-
- await responseProvider[routeMock.id].mockDelay();
-
// Use the handler defined for the HTTP Mocked interface (not the one passed on input to
// the factory) for retrieving the response value because that one could have had its
// response value manipulated by the individual test case.
- return responseProvider[routeMock.id](...args);
+
+ markApiCallAsHandled(responseProvider[routeMock.id].mockDelay);
+ await responseProvider[routeMock.id].mockDelay();
+
+ const fetchOptions: HttpFetchOptionsWithPath = isHttpFetchOptionsWithPath(args[0])
+ ? args[0]
+ : {
+ // Ignore below is needed because the http service methods are defined via an overloaded interface.
+ // If the first argument is NOT fetch with options, then we know that its a string and `args` has
+ // a potential for being of `.length` 2.
+ // @ts-ignore
+ ...(args[1] || {}),
+ path: args[0],
+ };
+
+ return responseProvider[routeMock.id](fetchOptions);
} else if (priorMockedFunction) {
return priorMockedFunction(...args);
}
diff --git a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx
index fc5c19e95fb77..d677a4a9fd662 100644
--- a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx
@@ -140,6 +140,22 @@ describe('when using PaginatedContent', () => {
});
});
+ it('should call onChange when page is empty', () => {
+ render({
+ pagination: {
+ pageIndex: 1,
+ pageSizeOptions: [5, 10, 20],
+ pageSize: 10,
+ totalItemCount: 10,
+ },
+ });
+ expect(onChangeHandler).toHaveBeenCalledWith({
+ pageIndex: 0,
+ pageSize: 10,
+ });
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
+ });
+
it('should ignore items, error, noItemsMessage when `children` is used', () => {
render({ children: {'children being used here'}
});
expect(renderResult.getByTestId('custom-content')).not.toBeNull();
diff --git a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx
index 890b21624eaf6..a6b2683316efe 100644
--- a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx
@@ -16,6 +16,7 @@ import React, {
useCallback,
useMemo,
useState,
+ useEffect,
} from 'react';
import {
CommonProps,
@@ -152,6 +153,12 @@ export const PaginatedContent = memo(
[pagination?.pageSize, pagination?.totalItemCount]
);
+ useEffect(() => {
+ if (pageCount > 0 && pageCount < (pagination?.pageIndex || 0) + 1) {
+ onChange({ pageIndex: pageCount - 1, pageSize: pagination?.pageSize || 0 });
+ }
+ }, [pageCount, onChange, pagination]);
+
const handleItemsPerPageChange: EuiTablePaginationProps['onChangeItemsPerPage'] = useCallback(
(pageSize) => {
onChange({ pageSize, pageIndex: pagination?.pageIndex || 0 });
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
index 3a3ad47f9f575..de05fa949b487 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
@@ -23,7 +23,16 @@ import {
HOST_METADATA_GET_ROUTE,
HOST_METADATA_LIST_ROUTE,
} from '../../../../common/endpoint/constants';
-import { AGENT_POLICY_API_ROUTES, GetAgentPoliciesResponse } from '../../../../../fleet/common';
+import {
+ AGENT_POLICY_API_ROUTES,
+ EPM_API_ROUTES,
+ GetAgentPoliciesResponse,
+ GetPackagesResponse,
+} from '../../../../../fleet/common';
+import {
+ PendingActionsHttpMockInterface,
+ pendingActionsHttpMock,
+} from '../../../common/lib/endpoint_pending_actions/mocks';
type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{
metadataList: () => HostResultList;
@@ -40,11 +49,15 @@ export const endpointMetadataHttpMocks = httpHandlerMockFactory {
- return {
+ const endpoint = {
metadata: generator.generateHostMetadata(),
host_status: HostStatus.UNHEALTHY,
query_strategy_version: MetadataQueryStrategyVersions.VERSION_2,
};
+
+ generator.updateCommonInfo();
+
+ return endpoint;
}),
total: 10,
request_page_size: 10,
@@ -88,6 +101,7 @@ export const endpointPolicyResponseHttpMock = httpHandlerMockFactory GetAgentPoliciesResponse;
+ packageList: () => GetPackagesResponse;
}>;
export const fleetApisHttpMock = httpHandlerMockFactory([
{
@@ -113,11 +127,24 @@ export const fleetApisHttpMock = httpHandlerMockFactory & {
- payload: EndpointState['endpointDetails']['activityLog'];
+ payload: EndpointState['endpointDetails']['activityLog']['logData'];
+};
+
+export type EndpointPendingActionsStateChanged = Action<'endpointPendingActionsStateChanged'> & {
+ payload: EndpointState['endpointPendingActions'];
};
export type EndpointAction =
@@ -157,6 +168,7 @@ export type EndpointAction =
| ServerFailedToReturnEndpointList
| ServerReturnedEndpointDetails
| ServerFailedToReturnEndpointDetails
+ | AppRequestedEndpointActivityLog
| EndpointDetailsActivityLogChanged
| ServerReturnedEndpointPolicyResponse
| ServerFailedToReturnEndpointPolicyResponse
@@ -178,4 +190,5 @@ export type EndpointAction =
| ServerFailedToReturnAgenstWithEndpointsTotal
| ServerFailedToReturnEndpointsTotal
| EndpointIsolationRequest
- | EndpointIsolationRequestStateChange;
+ | EndpointIsolationRequestStateChange
+ | EndpointPendingActionsStateChanged;
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts
index d5416d9f8ec96..d43f361a0e6bb 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts
@@ -7,7 +7,7 @@
import { Immutable } from '../../../../../common/endpoint/types';
import { DEFAULT_POLL_INTERVAL } from '../../../common/constants';
-import { createUninitialisedResourceState } from '../../../state';
+import { createLoadedResourceState, createUninitialisedResourceState } from '../../../state';
import { EndpointState } from '../types';
export const initialEndpointPageState = (): Immutable => {
@@ -19,7 +19,11 @@ export const initialEndpointPageState = (): Immutable => {
loading: false,
error: undefined,
endpointDetails: {
- activityLog: createUninitialisedResourceState(),
+ activityLog: {
+ page: 1,
+ pageSize: 50,
+ logData: createUninitialisedResourceState(),
+ },
hostDetails: {
details: undefined,
detailsLoading: false,
@@ -49,5 +53,6 @@ export const initialEndpointPageState = (): Immutable => {
policyVersionInfo: undefined,
hostStatus: undefined,
isolationRequestState: createUninitialisedResourceState(),
+ endpointPendingActions: createLoadedResourceState(new Map()),
};
};
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts
index 5be67a3581c9e..7f7c5f84f8bff 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts
@@ -43,7 +43,9 @@ describe('EndpointList store concerns', () => {
error: undefined,
endpointDetails: {
activityLog: {
- type: 'UninitialisedResourceState',
+ page: 1,
+ pageSize: 50,
+ logData: { type: 'UninitialisedResourceState' },
},
hostDetails: {
details: undefined,
@@ -75,6 +77,10 @@ describe('EndpointList store concerns', () => {
isolationRequestState: {
type: 'UninitialisedResourceState',
},
+ endpointPendingActions: {
+ data: new Map(),
+ type: 'LoadedResourceState',
+ },
});
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
index 98ef5a341ac9e..52da30fabf95a 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
@@ -18,7 +18,7 @@ import {
Immutable,
HostResultList,
HostIsolationResponse,
- EndpointAction,
+ ActivityLog,
ISOLATION_ACTIONS,
} from '../../../../../common/endpoint/types';
import { AppAction } from '../../../../common/store/actions';
@@ -43,6 +43,7 @@ import {
hostIsolationResponseMock,
} from '../../../../common/lib/endpoint_isolation/mocks';
import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator';
+import { endpointPageHttpMock } from '../mocks';
jest.mock('../../policy/store/services/ingest', () => ({
sendGetAgentConfigList: () => Promise.resolve({ items: [] }),
@@ -55,6 +56,7 @@ jest.mock('../../../../common/lib/kibana');
type EndpointListStore = Store, Immutable>;
describe('endpoint list middleware', () => {
+ const getKibanaServicesMock = KibanaServices.get as jest.Mock;
let fakeCoreStart: jest.Mocked;
let depsStart: DepsStartMock;
let fakeHttpServices: jest.Mocked;
@@ -69,6 +71,17 @@ describe('endpoint list middleware', () => {
return mockEndpointResultList({ request_page_size: 1, request_page_index: 1, total: 10 });
};
+ const dispatchUserChangedUrlToEndpointList = (locationOverrides: Partial = {}) => {
+ dispatch({
+ type: 'userChangedUrl',
+ payload: {
+ ...history.location,
+ pathname: getEndpointListPath({ name: 'endpointList' }),
+ ...locationOverrides,
+ },
+ });
+ };
+
beforeEach(() => {
fakeCoreStart = coreMock.createStart({ basePath: '/mock' });
depsStart = depsStartMock();
@@ -81,6 +94,7 @@ describe('endpoint list middleware', () => {
getState = store.getState;
dispatch = store.dispatch;
history = createBrowserHistory();
+ getKibanaServicesMock.mockReturnValue(fakeCoreStart);
});
it('handles `userChangedUrl`', async () => {
@@ -88,13 +102,7 @@ describe('endpoint list middleware', () => {
fakeHttpServices.post.mockResolvedValue(apiResponse);
expect(fakeHttpServices.post).not.toHaveBeenCalled();
- dispatch({
- type: 'userChangedUrl',
- payload: {
- ...history.location,
- pathname: getEndpointListPath({ name: 'endpointList' }),
- },
- });
+ dispatchUserChangedUrlToEndpointList();
await waitForAction('serverReturnedEndpointList');
expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', {
body: JSON.stringify({
@@ -111,13 +119,7 @@ describe('endpoint list middleware', () => {
expect(fakeHttpServices.post).not.toHaveBeenCalled();
// First change the URL
- dispatch({
- type: 'userChangedUrl',
- payload: {
- ...history.location,
- pathname: getEndpointListPath({ name: 'endpointList' }),
- },
- });
+ dispatchUserChangedUrlToEndpointList();
await waitForAction('serverReturnedEndpointList');
// Then request the Endpoint List
@@ -135,7 +137,6 @@ describe('endpoint list middleware', () => {
});
describe('handling of IsolateEndpointHost action', () => {
- const getKibanaServicesMock = KibanaServices.get as jest.Mock;
const dispatchIsolateEndpointHost = (action: ISOLATION_ACTIONS = 'isolate') => {
dispatch({
type: 'endpointIsolationRequest',
@@ -149,7 +150,6 @@ describe('endpoint list middleware', () => {
beforeEach(() => {
isolateApiResponseHandlers = hostIsolationHttpMocks(fakeHttpServices);
- getKibanaServicesMock.mockReturnValue(fakeCoreStart);
});
it('should set Isolation state to loading', async () => {
@@ -224,25 +224,42 @@ describe('endpoint list middleware', () => {
selected_endpoint: endpointList.hosts[0].metadata.agent.id,
});
const dispatchUserChangedUrl = () => {
- dispatch({
- type: 'userChangedUrl',
- payload: {
- ...history.location,
- pathname: '/endpoints',
- search: `?${search.split('?').pop()}`,
- },
- });
+ dispatchUserChangedUrlToEndpointList({ search: `?${search.split('?').pop()}` });
};
+
const fleetActionGenerator = new FleetActionGenerator(Math.random().toString());
- const activityLog = [
- fleetActionGenerator.generate({
- agents: [endpointList.hosts[0].metadata.agent.id],
- }),
- ];
+ const actionData = fleetActionGenerator.generate({
+ agents: [endpointList.hosts[0].metadata.agent.id],
+ });
+ const responseData = fleetActionGenerator.generateResponse({
+ agent_id: endpointList.hosts[0].metadata.agent.id,
+ });
+ const getMockEndpointActivityLog = () =>
+ ({
+ total: 2,
+ page: 1,
+ pageSize: 50,
+ data: [
+ {
+ type: 'response',
+ item: {
+ id: '',
+ data: responseData,
+ },
+ },
+ {
+ type: 'action',
+ item: {
+ id: '',
+ data: actionData,
+ },
+ },
+ ],
+ } as ActivityLog);
const dispatchGetActivityLog = () => {
dispatch({
type: 'endpointDetailsActivityLogChanged',
- payload: createLoadedResourceState(activityLog),
+ payload: createLoadedResourceState(getMockEndpointActivityLog()),
});
};
@@ -270,11 +287,45 @@ describe('endpoint list middleware', () => {
dispatchGetActivityLog();
const loadedDispatchedResponse = await loadedDispatched;
- const activityLogData = (loadedDispatchedResponse.payload as LoadedResourceState<
- EndpointAction[]
- >).data;
+ const activityLogData = (loadedDispatchedResponse.payload as LoadedResourceState)
+ .data;
- expect(activityLogData).toEqual(activityLog);
+ expect(activityLogData).toEqual(getMockEndpointActivityLog());
+ });
+ });
+
+ describe('handle Endpoint Pending Actions state actions', () => {
+ let mockedApis: ReturnType;
+
+ beforeEach(() => {
+ mockedApis = endpointPageHttpMock(fakeHttpServices);
+ });
+
+ it('should include all agents ids from the list when calling API', async () => {
+ const loadingPendingActions = waitForAction('endpointPendingActionsStateChanged', {
+ validate: (action) => isLoadedResourceState(action.payload),
+ });
+
+ dispatchUserChangedUrlToEndpointList();
+ await loadingPendingActions;
+
+ expect(mockedApis.responseProvider.pendingActions).toHaveBeenCalledWith({
+ path: expect.any(String),
+ query: {
+ agent_ids: [
+ '6db499e5-4927-4350-abb8-d8318e7d0eec',
+ 'c082dda9-1847-4997-8eda-f1192d95bec3',
+ '8aa1cd61-cc25-4783-afb5-0eefc4919c07',
+ '47fe24c1-7370-419a-9732-3ff38bf41272',
+ '0d2b2fa7-a9cd-49fc-ad5f-0252c642290e',
+ 'f480092d-0445-4bf3-9c96-8a3d5cb97824',
+ '3850e676-0940-4c4b-aaca-571bd1bc66d9',
+ '46efcc7a-086a-47a3-8f09-c4ecd6d2d917',
+ 'afa55826-b81b-4440-a2ac-0644d77a3fc6',
+ '25b49e50-cb5c-43df-824f-67b8cf697d9d',
+ ],
+ },
+ });
});
});
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
index a1da3c072293e..4f96223e8b789 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
@@ -8,7 +8,7 @@
import { Dispatch } from 'redux';
import { CoreStart, HttpStart } from 'kibana/public';
import {
- EndpointAction,
+ ActivityLog,
HostInfo,
HostIsolationRequestBody,
HostIsolationResponse,
@@ -32,8 +32,11 @@ import {
getIsIsolationRequestPending,
getCurrentIsolationRequestState,
getActivityLogData,
+ getActivityLogDataPaging,
+ getLastLoadedActivityLogData,
+ detailsData,
} from './selectors';
-import { EndpointState, PolicyIds } from '../types';
+import { AgentIdsPendingActions, EndpointState, PolicyIds } from '../types';
import {
sendGetEndpointSpecificPackagePolicies,
sendGetEndpointSecurityPackage,
@@ -57,9 +60,13 @@ import { isolateHost, unIsolateHost } from '../../../../common/lib/endpoint_isol
import { AppAction } from '../../../../common/store/actions';
import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables';
import { ServerReturnedEndpointPackageInfo } from './action';
+import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions';
type EndpointPageStore = ImmutableMiddlewareAPI;
+// eslint-disable-next-line no-console
+const logError = console.error;
+
export const endpointMiddlewareFactory: ImmutableMiddlewareFactory = (
coreStart,
depsStart
@@ -108,6 +115,8 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(getActivityLogData(getState())),
+ payload: createLoadingResourceState(getActivityLogData(getState())),
});
try {
- const activityLog = await coreStart.http.get(
- resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { agent_id: selectedAgent(getState()) })
- );
+ const { page, pageSize } = getActivityLogDataPaging(getState());
+ const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, {
+ agent_id: selectedAgent(getState()),
+ });
+ const activityLog = await coreStart.http.get(route, {
+ query: { page, page_size: pageSize },
+ });
dispatch({
type: 'endpointDetailsActivityLogChanged',
- payload: createLoadedResourceState(activityLog),
+ payload: createLoadedResourceState(activityLog),
});
} catch (error) {
dispatch({
type: 'endpointDetailsActivityLogChanged',
- payload: createFailedResourceState(error.body ?? error),
+ payload: createFailedResourceState(error.body ?? error),
});
}
@@ -371,6 +383,56 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(getActivityLogData(getState())),
+ });
+
+ try {
+ const { page, pageSize } = getActivityLogDataPaging(getState());
+ const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, {
+ agent_id: selectedAgent(getState()),
+ });
+ const activityLog = await coreStart.http.get(route, {
+ query: { page, page_size: pageSize },
+ });
+
+ const lastLoadedLogData = getLastLoadedActivityLogData(getState());
+ if (lastLoadedLogData !== undefined) {
+ const updatedLogDataItems = [
+ ...new Set([...lastLoadedLogData.data, ...activityLog.data]),
+ ] as ActivityLog['data'];
+
+ const updatedLogData = {
+ total: activityLog.total,
+ page: activityLog.page,
+ pageSize: activityLog.pageSize,
+ data: updatedLogDataItems,
+ };
+ dispatch({
+ type: 'endpointDetailsActivityLogChanged',
+ payload: createLoadedResourceState(updatedLogData),
+ });
+ // TODO dispatch 'noNewLogData' if !activityLog.length
+ // resets paging to previous state
+ } else {
+ dispatch({
+ type: 'endpointDetailsActivityLogChanged',
+ payload: createLoadedResourceState(activityLog),
+ });
+ }
+ } catch (error) {
+ dispatch({
+ type: 'endpointDetailsActivityLogChanged',
+ payload: createFailedResourceState(error.body ?? error),
+ });
+ }
+ }
+
// Isolate Host
if (action.type === 'endpointIsolationRequest') {
return handleIsolateEndpointHost(store, action);
@@ -456,10 +518,8 @@ const endpointsTotal = async (http: HttpStart): Promise => {
})
).total;
} catch (error) {
- // eslint-disable-next-line no-console
- console.error(`error while trying to check for total endpoints`);
- // eslint-disable-next-line no-console
- console.error(error);
+ logError(`error while trying to check for total endpoints`);
+ logError(error);
}
return 0;
};
@@ -468,10 +528,8 @@ const doEndpointsExist = async (http: HttpStart): Promise => {
try {
return (await endpointsTotal(http)) > 0;
} catch (error) {
- // eslint-disable-next-line no-console
- console.error(`error while trying to check if endpoints exist`);
- // eslint-disable-next-line no-console
- console.error(error);
+ logError(`error while trying to check if endpoints exist`);
+ logError(error);
}
return false;
};
@@ -530,7 +588,51 @@ async function getEndpointPackageInfo(
});
} catch (error) {
// Ignore Errors, since this should not hinder the user's ability to use the UI
- // eslint-disable-next-line no-console
- console.error(error);
+ logError(error);
}
}
+
+/**
+ * retrieves the Endpoint pending actions for all of the existing endpoints being displayed on the list
+ * or the details tab.
+ *
+ * @param store
+ */
+const loadEndpointsPendingActions = async ({
+ getState,
+ dispatch,
+}: EndpointPageStore): Promise => {
+ const state = getState();
+ const detailsEndpoint = detailsData(state);
+ const listEndpoints = listData(state);
+ const agentsIds = new Set();
+
+ // get all agent ids for the endpoints in the list
+ if (detailsEndpoint) {
+ agentsIds.add(detailsEndpoint.elastic.agent.id);
+ }
+
+ for (const endpointInfo of listEndpoints) {
+ agentsIds.add(endpointInfo.metadata.elastic.agent.id);
+ }
+
+ if (agentsIds.size === 0) {
+ return;
+ }
+
+ try {
+ const { data: pendingActions } = await fetchPendingActionsByAgentId(Array.from(agentsIds));
+ const agentIdToPendingActions: AgentIdsPendingActions = new Map();
+
+ for (const pendingAction of pendingActions) {
+ agentIdToPendingActions.set(pendingAction.agent_id, pendingAction.pending_actions);
+ }
+
+ dispatch({
+ type: 'endpointPendingActionsStateChanged',
+ payload: createLoadedResourceState(agentIdToPendingActions),
+ });
+ } catch (error) {
+ logError(error);
+ }
+};
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts
index 19235b792b270..9460c27dfe705 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EndpointDetailsActivityLogChanged } from './action';
+import { EndpointDetailsActivityLogChanged, EndpointPendingActionsStateChanged } from './action';
import {
isOnEndpointPage,
hasSelectedEndpoint,
@@ -33,11 +33,27 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer = (
+ state,
+ action
+) => {
+ if (isOnEndpointPage(state)) {
+ return {
+ ...state,
+ endpointPendingActions: action.payload,
+ };
+ }
+ return state;
+};
+
/* eslint-disable-next-line complexity */
export const endpointListReducer: StateReducer = (state = initialEndpointPageState(), action) => {
if (action.type === 'serverReturnedEndpointList') {
@@ -121,8 +137,25 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
},
},
};
+ } else if (action.type === 'appRequestedEndpointActivityLog') {
+ const pageData = {
+ page: action.payload.page,
+ pageSize: action.payload.pageSize,
+ };
+ return {
+ ...state,
+ endpointDetails: {
+ ...state.endpointDetails!,
+ activityLog: {
+ ...state.endpointDetails.activityLog,
+ ...pageData,
+ },
+ },
+ };
} else if (action.type === 'endpointDetailsActivityLogChanged') {
return handleEndpointDetailsActivityLogChanged(state, action);
+ } else if (action.type === 'endpointPendingActionsStateChanged') {
+ return handleEndpointPendingActionsStateChanged(state, action);
} else if (action.type === 'serverReturnedPoliciesForOnboarding') {
return {
...state,
@@ -220,6 +253,12 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
policyResponseError: undefined,
};
+ const activityLog = {
+ logData: createUninitialisedResourceState(),
+ page: 1,
+ pageSize: 50,
+ };
+
// Reset `isolationRequestState` if needed
if (
uiQueryParams(newState).show !== 'isolate' &&
@@ -236,6 +275,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
...stateUpdates,
endpointDetails: {
...state.endpointDetails,
+ activityLog,
hostDetails: {
...state.endpointDetails.hostDetails,
detailsError: undefined,
@@ -253,6 +293,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
...stateUpdates,
endpointDetails: {
...state.endpointDetails,
+ activityLog,
hostDetails: {
...state.endpointDetails.hostDetails,
detailsLoading: true,
@@ -269,6 +310,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
...stateUpdates,
endpointDetails: {
...state.endpointDetails,
+ activityLog,
hostDetails: {
...state.endpointDetails.hostDetails,
detailsLoading: true,
@@ -287,6 +329,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
...stateUpdates,
endpointDetails: {
...state.endpointDetails,
+ activityLog,
hostDetails: {
...state.endpointDetails.hostDetails,
detailsError: undefined,
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
index f3848557567ec..d9be85377c81d 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
@@ -17,6 +17,8 @@ import {
HostPolicyResponseActionStatus,
MetadataQueryStrategyVersions,
HostStatus,
+ ActivityLog,
+ HostMetadata,
} from '../../../../../common/endpoint/types';
import { EndpointState, EndpointIndexUIQueryParams } from '../types';
import { extractListPaginationParams } from '../../../common/routing';
@@ -27,12 +29,15 @@ import {
} from '../../../common/constants';
import { Query } from '../../../../../../../../src/plugins/data/common/query/types';
import {
+ getLastLoadedResourceState,
isFailedResourceState,
isLoadedResourceState,
isLoadingResourceState,
} from '../../../state';
+
import { ServerApiError } from '../../../../common/types';
import { isEndpointHostIsolated } from '../../../../common/utils/validators';
+import { EndpointHostIsolationStatusProps } from '../../../../common/components/endpoint/host_isolation';
export const listData = (state: Immutable) => state.hosts;
@@ -359,9 +364,25 @@ export const getIsolationRequestError: (
}
});
+export const getActivityLogDataPaging = (
+ state: Immutable
+): Immutable> => {
+ return {
+ page: state.endpointDetails.activityLog.page,
+ pageSize: state.endpointDetails.activityLog.pageSize,
+ };
+};
+
export const getActivityLogData = (
state: Immutable
-): Immutable => state.endpointDetails.activityLog;
+): Immutable =>
+ state.endpointDetails.activityLog.logData;
+
+export const getLastLoadedActivityLogData: (
+ state: Immutable
+) => Immutable | undefined = createSelector(getActivityLogData, (activityLog) => {
+ return getLastLoadedResourceState(activityLog)?.data;
+});
export const getActivityLogRequestLoading: (
state: Immutable
@@ -375,6 +396,13 @@ export const getActivityLogRequestLoaded: (
isLoadedResourceState(activityLog)
);
+export const getActivityLogIterableData: (
+ state: Immutable
+) => Immutable = createSelector(getActivityLogData, (activityLog) => {
+ const emptyArray: ActivityLog['data'] = [];
+ return isLoadedResourceState(activityLog) ? activityLog.data.data : emptyArray;
+});
+
export const getActivityLogError: (
state: Immutable
) => ServerApiError | undefined = createSelector(getActivityLogData, (activityLog) => {
@@ -386,3 +414,40 @@ export const getActivityLogError: (
export const getIsEndpointHostIsolated = createSelector(detailsData, (details) => {
return (details && isEndpointHostIsolated(details)) || false;
});
+
+export const getEndpointPendingActionsState = (
+ state: Immutable
+): Immutable => {
+ return state.endpointPendingActions;
+};
+
+/**
+ * Returns a function (callback) that can be used to retrieve the props for the `EndpointHostIsolationStatus`
+ * component for a given Endpoint
+ */
+export const getEndpointHostIsolationStatusPropsCallback: (
+ state: Immutable
+) => (endpoint: HostMetadata) => EndpointHostIsolationStatusProps = createSelector(
+ getEndpointPendingActionsState,
+ (pendingActionsState) => {
+ return (endpoint: HostMetadata) => {
+ let pendingIsolate = 0;
+ let pendingUnIsolate = 0;
+
+ if (isLoadedResourceState(pendingActionsState)) {
+ const endpointPendingActions = pendingActionsState.data.get(endpoint.elastic.agent.id);
+
+ if (endpointPendingActions) {
+ pendingIsolate = endpointPendingActions?.isolate ?? 0;
+ pendingUnIsolate = endpointPendingActions?.unisolate ?? 0;
+ }
+ }
+
+ return {
+ isIsolated: isEndpointHostIsolated(endpoint),
+ pendingIsolate,
+ pendingUnIsolate,
+ };
+ };
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
index 53ddfaee7aa05..59aa2bd15dd74 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
@@ -6,6 +6,7 @@
*/
import {
+ ActivityLog,
HostInfo,
Immutable,
HostMetadata,
@@ -14,8 +15,8 @@ import {
PolicyData,
MetadataQueryStrategyVersions,
HostStatus,
- EndpointAction,
HostIsolationResponse,
+ EndpointPendingActions,
} from '../../../../common/endpoint/types';
import { ServerApiError } from '../../../common/types';
import { GetPackagesResponse } from '../../../../../fleet/common';
@@ -36,7 +37,11 @@ export interface EndpointState {
/** api error from retrieving host list */
error?: ServerApiError;
endpointDetails: {
- activityLog: AsyncResourceState;
+ activityLog: {
+ page: number;
+ pageSize: number;
+ logData: AsyncResourceState;
+ };
hostDetails: {
/** details data for a specific host */
details?: Immutable;
@@ -90,10 +95,18 @@ export interface EndpointState {
policyVersionInfo?: HostInfo['policy_info'];
/** The status of the host, which is mapped to the Elastic Agent status in Fleet */
hostStatus?: HostStatus;
- /* Host isolation state */
+ /** Host isolation request state for a single endpoint */
isolationRequestState: AsyncResourceState;
+ /**
+ * Holds a map of `agentId` to `EndpointPendingActions` that is used by both the list and details view
+ * Getting pending endpoint actions is "supplemental" data, so there is no need to show other Async
+ * states other than Loaded
+ */
+ endpointPendingActions: AsyncResourceState;
}
+export type AgentIdsPendingActions = Map;
+
/**
* packagePolicy contains a list of Package Policy IDs (received via Endpoint metadata policy response) mapped to a boolean whether they exist or not.
* agentPolicy contains a list of existing Package Policy Ids mapped to an associated Fleet parent Agent Config.
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx
new file mode 100644
index 0000000000000..9010bb5785c1d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx
@@ -0,0 +1,90 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import {
+ AppContextTestRender,
+ createAppRootMockRenderer,
+} from '../../../../../common/mock/endpoint';
+import { endpointPageHttpMock } from '../../mocks';
+import { act } from '@testing-library/react';
+import { EndpointAgentStatus, EndpointAgentStatusProps } from './endpoint_agent_status';
+import { HostMetadata, HostStatus } from '../../../../../../common/endpoint/types';
+import { isLoadedResourceState } from '../../../../state';
+import { KibanaServices } from '../../../../../common/lib/kibana';
+
+jest.mock('../../../../../common/lib/kibana');
+
+describe('When using the EndpointAgentStatus component', () => {
+ let render: (
+ props: EndpointAgentStatusProps
+ ) => Promise>;
+ let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction'];
+ let renderResult: ReturnType;
+ let httpMocks: ReturnType;
+ let endpointMeta: HostMetadata;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ (KibanaServices.get as jest.Mock).mockReturnValue(mockedContext.startServices);
+ httpMocks = endpointPageHttpMock(mockedContext.coreStart.http);
+ waitForAction = mockedContext.middlewareSpy.waitForAction;
+ endpointMeta = httpMocks.responseProvider.metadataList().hosts[0].metadata;
+ render = async (props: EndpointAgentStatusProps) => {
+ renderResult = mockedContext.render( );
+ return renderResult;
+ };
+
+ act(() => {
+ mockedContext.history.push('/endpoints');
+ });
+ });
+
+ it.each([
+ ['Healthy', 'healthy'],
+ ['Unhealthy', 'unhealthy'],
+ ['Updating', 'updating'],
+ ['Offline', 'offline'],
+ ['Inactive', 'inactive'],
+ ['Unhealthy', 'someUnknownValueHere'],
+ ])('should show agent status of %s', async (expectedLabel, hostStatus) => {
+ await render({ hostStatus: hostStatus as HostStatus, endpointMetadata: endpointMeta });
+ expect(renderResult.getByTestId('rowHostStatus').textContent).toEqual(expectedLabel);
+ });
+
+ describe('and host is isolated or pending isolation', () => {
+ beforeEach(async () => {
+ // Ensure pending action api sets pending action for the test endpoint metadata
+ const pendingActionsResponseProvider = httpMocks.responseProvider.pendingActions.getMockImplementation();
+ httpMocks.responseProvider.pendingActions.mockImplementation((...args) => {
+ const response = pendingActionsResponseProvider!(...args);
+ response.data.some((pendingAction) => {
+ if (pendingAction.agent_id === endpointMeta.elastic.agent.id) {
+ pendingAction.pending_actions.isolate = 1;
+ return true;
+ }
+ return false;
+ });
+ return response;
+ });
+
+ const loadingPendingActions = waitForAction('endpointPendingActionsStateChanged', {
+ validate: (action) => isLoadedResourceState(action.payload),
+ });
+
+ await render({ hostStatus: HostStatus.HEALTHY, endpointMetadata: endpointMeta });
+ await loadingPendingActions;
+ });
+
+ it('should show host pending action', () => {
+ expect(renderResult.getByTestId('rowIsolationStatus').textContent).toEqual(
+ 'Isolating pending'
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.tsx
new file mode 100644
index 0000000000000..94db233972d67
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo } from 'react';
+import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import styled from 'styled-components';
+import { HostInfo, HostMetadata } from '../../../../../../common/endpoint/types';
+import { HOST_STATUS_TO_BADGE_COLOR } from '../host_constants';
+import { EndpointHostIsolationStatus } from '../../../../../common/components/endpoint/host_isolation';
+import { useEndpointSelector } from '../hooks';
+import { getEndpointHostIsolationStatusPropsCallback } from '../../store/selectors';
+
+const EuiFlexGroupStyled = styled(EuiFlexGroup)`
+ .isolation-status {
+ margin-left: ${({ theme }) => theme.eui.paddingSizes.s};
+ }
+`;
+
+export interface EndpointAgentStatusProps {
+ hostStatus: HostInfo['host_status'];
+ endpointMetadata: HostMetadata;
+}
+export const EndpointAgentStatus = memo(
+ ({ endpointMetadata, hostStatus }) => {
+ const getEndpointIsolationStatusProps = useEndpointSelector(
+ getEndpointHostIsolationStatusPropsCallback
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+);
+
+EndpointAgentStatus.displayName = 'EndpointAgentStatus';
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx
index 7ecbad54dbbec..356d44a810528 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx
@@ -70,6 +70,7 @@ describe('When using the Endpoint Details Actions Menu', () => {
['View host details', 'hostLink'],
['View agent policy', 'agentPolicyLink'],
['View agent details', 'agentDetailsLink'],
+ ['Reassign agent policy', 'agentPolicyReassignLink'],
])('should display %s action', async (_, dataTestSubj) => {
await render();
expect(renderResult.getByTestId(dataTestSubj)).not.toBeNull();
@@ -80,6 +81,7 @@ describe('When using the Endpoint Details Actions Menu', () => {
['View host details', 'hostLink'],
['View agent policy', 'agentPolicyLink'],
['View agent details', 'agentDetailsLink'],
+ ['Reassign agent policy', 'agentPolicyReassignLink'],
])(
'should navigate via kibana `navigateToApp()` when %s is clicked',
async (_, dataTestSubj) => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
index de6d2ecf36ecc..c431cd682d25b 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
@@ -5,53 +5,161 @@
* 2.0.
*/
-import React, { memo } from 'react';
-
-import { EuiAvatar, EuiComment, EuiText } from '@elastic/eui';
-import { Immutable, EndpointAction } from '../../../../../../../common/endpoint/types';
-import { FormattedDateAndTime } from '../../../../../../common/components/endpoint/formatted_date_time';
-import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme';
-
-export const LogEntry = memo(
- ({ endpointAction }: { endpointAction: Immutable }) => {
- const euiTheme = useEuiTheme();
- const isIsolated = endpointAction?.data.command === 'isolate';
-
- // do this better when we can distinguish between endpoint events vs user events
- const iconType = endpointAction.user_id === 'sys' ? 'dot' : isIsolated ? 'lock' : 'lockOpen';
- const commentType = endpointAction.user_id === 'sys' ? 'update' : 'regular';
- const timelineIcon = (
-
- );
- const event = `${isIsolated ? 'isolated' : 'unisolated'} host`;
- const hasComment = !!endpointAction.data.comment;
-
- return (
-
- {hasComment ? (
-
- {endpointAction.data.comment}
-
- ) : undefined}
-
- );
+import React, { memo, useMemo } from 'react';
+import styled from 'styled-components';
+
+import { EuiComment, EuiText, EuiAvatarProps, EuiCommentProps, IconType } from '@elastic/eui';
+import { Immutable, ActivityLogEntry } from '../../../../../../../common/endpoint/types';
+import { FormattedRelativePreferenceDate } from '../../../../../../common/components/formatted_date';
+import { LogEntryTimelineIcon } from './log_entry_timeline_icon';
+
+import * as i18 from '../../translations';
+
+const useLogEntryUIProps = (
+ logEntry: Immutable
+): {
+ actionEventTitle: string;
+ avatarSize: EuiAvatarProps['size'];
+ commentText: string;
+ commentType: EuiCommentProps['type'];
+ displayComment: boolean;
+ displayResponseEvent: boolean;
+ iconType: IconType;
+ isResponseEvent: boolean;
+ isSuccessful: boolean;
+ responseEventTitle: string;
+ username: string | React.ReactNode;
+} => {
+ return useMemo(() => {
+ let iconType: IconType = 'dot';
+ let commentType: EuiCommentProps['type'] = 'update';
+ let commentText: string = '';
+ let avatarSize: EuiAvatarProps['size'] = 's';
+ let isIsolateAction: boolean = false;
+ let isResponseEvent: boolean = false;
+ let isSuccessful: boolean = false;
+ let displayComment: boolean = false;
+ let displayResponseEvent: boolean = true;
+ let username: EuiCommentProps['username'] = '';
+
+ if (logEntry.type === 'action') {
+ avatarSize = 'm';
+ commentType = 'regular';
+ commentText = logEntry.item.data.data.comment ?? '';
+ displayResponseEvent = false;
+ iconType = 'lockOpen';
+ username = logEntry.item.data.user_id;
+ if (logEntry.item.data.data) {
+ const data = logEntry.item.data.data;
+ if (data.command === 'isolate') {
+ iconType = 'lock';
+ isIsolateAction = true;
+ }
+ if (data.comment) {
+ displayComment = true;
+ }
+ }
+ } else if (logEntry.type === 'response') {
+ isResponseEvent = true;
+ if (logEntry.item.data.action_data.command === 'isolate') {
+ isIsolateAction = true;
+ }
+ if (!!logEntry.item.data.completed_at && !logEntry.item.data.error) {
+ isSuccessful = true;
+ }
+ }
+
+ const actionEventTitle = isIsolateAction
+ ? i18.ACTIVITY_LOG.LogEntry.action.isolatedAction
+ : i18.ACTIVITY_LOG.LogEntry.action.unisolatedAction;
+
+ const getResponseEventTitle = () => {
+ if (isIsolateAction) {
+ if (isSuccessful) {
+ return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful;
+ } else {
+ return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful;
+ }
+ } else {
+ if (isSuccessful) {
+ return i18.ACTIVITY_LOG.LogEntry.response.unisolationSuccessful;
+ } else {
+ return i18.ACTIVITY_LOG.LogEntry.response.unisolationFailed;
+ }
+ }
+ };
+
+ return {
+ actionEventTitle,
+ avatarSize,
+ commentText,
+ commentType,
+ displayComment,
+ displayResponseEvent,
+ iconType,
+ isResponseEvent,
+ isSuccessful,
+ responseEventTitle: getResponseEventTitle(),
+ username,
+ };
+ }, [logEntry]);
+};
+
+const StyledEuiComment = styled(EuiComment)`
+ .euiCommentEvent__headerTimestamp {
+ display: flex;
+ :before {
+ content: '';
+ background-color: ${(props) => props.theme.eui.euiColorInk};
+ display: block;
+ width: ${(props) => props.theme.eui.euiBorderWidthThick};
+ height: ${(props) => props.theme.eui.euiBorderWidthThick};
+ margin: 0 ${(props) => props.theme.eui.euiSizeXS} 0 ${(props) => props.theme.eui.euiSizeS};
+ border-radius: 50%;
+ content: '';
+ margin: 0 8px 0 4px;
+ border-radius: 50%;
+ position: relative;
+ top: 10px;
+ }
}
-);
+`;
+
+export const LogEntry = memo(({ logEntry }: { logEntry: Immutable }) => {
+ const {
+ actionEventTitle,
+ avatarSize,
+ commentText,
+ commentType,
+ displayComment,
+ displayResponseEvent,
+ iconType,
+ isResponseEvent,
+ isSuccessful,
+ responseEventTitle,
+ username,
+ } = useLogEntryUIProps(logEntry);
+
+ return (
+ {displayResponseEvent ? responseEventTitle : actionEventTitle}}
+ timelineIcon={
+
+ }
+ data-test-subj="timelineEntry"
+ >
+ {displayComment ? (
+
+ {commentText}
+
+ ) : undefined}
+
+ );
+});
LogEntry.displayName = 'LogEntry';
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx
new file mode 100644
index 0000000000000..3ff311cd8a139
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo } from 'react';
+import { EuiAvatar, EuiAvatarProps } from '@elastic/eui';
+import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme';
+
+export const LogEntryTimelineIcon = memo(
+ ({
+ avatarSize,
+ isResponseEvent,
+ isSuccessful,
+ iconType,
+ }: {
+ avatarSize: EuiAvatarProps['size'];
+ isResponseEvent: boolean;
+ isSuccessful: boolean;
+ iconType: EuiAvatarProps['iconType'];
+ }) => {
+ const euiTheme = useEuiTheme();
+
+ return (
+
+ );
+ }
+);
+
+LogEntryTimelineIcon.displayName = 'LogEntryTimelineIcon';
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx
index 50c91730e332c..4395e3965ea00 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx
@@ -7,21 +7,48 @@
import React, { memo, useCallback } from 'react';
-import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
+import { EuiButton, EuiEmptyPrompt, EuiLoadingContent, EuiSpacer } from '@elastic/eui';
+import { useDispatch } from 'react-redux';
import { LogEntry } from './components/log_entry';
import * as i18 from '../translations';
import { SearchBar } from '../../../../components/search_bar';
-import { Immutable, EndpointAction } from '../../../../../../common/endpoint/types';
+import { Immutable, ActivityLog } from '../../../../../../common/endpoint/types';
import { AsyncResourceState } from '../../../../state';
+import { useEndpointSelector } from '../hooks';
+import { EndpointAction } from '../../store/action';
+import {
+ getActivityLogDataPaging,
+ getActivityLogError,
+ getActivityLogIterableData,
+ getActivityLogRequestLoaded,
+ getActivityLogRequestLoading,
+} from '../../store/selectors';
export const EndpointActivityLog = memo(
- ({ endpointActions }: { endpointActions: AsyncResourceState> }) => {
+ ({ activityLog }: { activityLog: AsyncResourceState> }) => {
+ const activityLogLoading = useEndpointSelector(getActivityLogRequestLoading);
+ const activityLogLoaded = useEndpointSelector(getActivityLogRequestLoaded);
+ const activityLogData = useEndpointSelector(getActivityLogIterableData);
+ const activityLogError = useEndpointSelector(getActivityLogError);
+ const dispatch = useDispatch<(a: EndpointAction) => void>();
+ const { page, pageSize } = useEndpointSelector(getActivityLogDataPaging);
// TODO
const onSearch = useCallback(() => {}, []);
+
+ const getActivityLog = useCallback(() => {
+ dispatch({
+ type: 'appRequestedEndpointActivityLog',
+ payload: {
+ page: page + 1,
+ pageSize,
+ },
+ });
+ }, [dispatch, page, pageSize]);
+
return (
<>
- {endpointActions.type !== 'LoadedResourceState' || !endpointActions.data.length ? (
+ {activityLogLoading || activityLogError ? (
- {endpointActions.data.map((endpointAction) => (
-
- ))}
+ {activityLogLoading ? (
+
+ ) : (
+ activityLogLoaded &&
+ activityLogData.map((logEntry) => (
+
+ ))
+ )}
+
+ {'show more'}
+
>
)}
>
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx
index 16cae79d42c0f..64ea575c37d79 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx
@@ -8,10 +8,8 @@
import styled from 'styled-components';
import {
EuiDescriptionList,
- EuiHorizontalRule,
EuiListGroup,
EuiListGroupItem,
- EuiIcon,
EuiText,
EuiFlexGroup,
EuiFlexItem,
@@ -23,19 +21,17 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { isPolicyOutOfDate } from '../../utils';
import { HostInfo, HostMetadata, HostStatus } from '../../../../../../common/endpoint/types';
-import { useEndpointSelector, useAgentDetailsIngestUrl } from '../hooks';
-import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
+import { useEndpointSelector } from '../hooks';
import { policyResponseStatus, uiQueryParams } from '../../store/selectors';
-import { POLICY_STATUS_TO_BADGE_COLOR, HOST_STATUS_TO_BADGE_COLOR } from '../host_constants';
+import { POLICY_STATUS_TO_BADGE_COLOR } from '../host_constants';
import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
-import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app';
import { getEndpointDetailsPath } from '../../../../common/routing';
import { SecurityPageName } from '../../../../../app/types';
import { useFormatUrl } from '../../../../../common/components/link_to';
-import { AgentDetailsReassignPolicyAction } from '../../../../../../../fleet/public';
import { EndpointPolicyLink } from '../components/endpoint_policy_link';
import { OutOfDate } from '../components/out_of_date';
+import { EndpointAgentStatus } from '../components/endpoint_agent_status';
const HostIds = styled(EuiListGroupItem)`
margin-top: 0;
@@ -44,8 +40,6 @@ const HostIds = styled(EuiListGroupItem)`
}
`;
-const openReassignFlyoutSearch = '?openReassignFlyout=true';
-
export const EndpointDetails = memo(
({
details,
@@ -56,19 +50,34 @@ export const EndpointDetails = memo(
policyInfo?: HostInfo['policy_info'];
hostStatus: HostStatus;
}) => {
- const agentId = details.elastic.agent.id;
- const {
- url: agentDetailsUrl,
- appId: ingestAppId,
- appPath: agentDetailsAppPath,
- } = useAgentDetailsIngestUrl(agentId);
const queryParams = useEndpointSelector(uiQueryParams);
const policyStatus = useEndpointSelector(
policyResponseStatus
) as keyof typeof POLICY_STATUS_TO_BADGE_COLOR;
const { formatUrl } = useFormatUrl(SecurityPageName.administration);
- const detailsResultsUpper = useMemo(() => {
+ const [policyResponseUri, policyResponseRoutePath] = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ const { selected_endpoint, show, ...currentUrlParams } = queryParams;
+ return [
+ formatUrl(
+ getEndpointDetailsPath({
+ name: 'endpointPolicyResponse',
+ ...currentUrlParams,
+ selected_endpoint: details.agent.id,
+ })
+ ),
+ getEndpointDetailsPath({
+ name: 'endpointPolicyResponse',
+ ...currentUrlParams,
+ selected_endpoint: details.agent.id,
+ }),
+ ];
+ }, [details.agent.id, formatUrl, queryParams]);
+
+ const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath);
+
+ const detailsResults = useMemo(() => {
return [
{
title: i18n.translate('xpack.securitySolution.endpoint.details.os', {
@@ -80,20 +89,7 @@ export const EndpointDetails = memo(
title: i18n.translate('xpack.securitySolution.endpoint.details.agentStatus', {
defaultMessage: 'Agent Status',
}),
- description: (
-
-
-
-
-
- ),
+ description: ,
},
{
title: i18n.translate('xpack.securitySolution.endpoint.details.lastSeen', {
@@ -106,55 +102,9 @@ export const EndpointDetails = memo(
),
},
- ];
- }, [details, hostStatus]);
-
- const [policyResponseUri, policyResponseRoutePath] = useMemo(() => {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const { selected_endpoint, show, ...currentUrlParams } = queryParams;
- return [
- formatUrl(
- getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
- ...currentUrlParams,
- selected_endpoint: details.agent.id,
- })
- ),
- getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
- ...currentUrlParams,
- selected_endpoint: details.agent.id,
- }),
- ];
- }, [details.agent.id, formatUrl, queryParams]);
-
- const agentDetailsWithFlyoutPath = `${agentDetailsAppPath}${openReassignFlyoutSearch}`;
- const agentDetailsWithFlyoutUrl = `${agentDetailsUrl}${openReassignFlyoutSearch}`;
- const handleReassignEndpointsClick = useNavigateToAppEventHandler(
- ingestAppId,
- {
- path: agentDetailsWithFlyoutPath,
- state: {
- onDoneNavigateTo: [
- 'securitySolution:administration',
- {
- path: getEndpointDetailsPath({
- name: 'endpointDetails',
- selected_endpoint: details.agent.id,
- }),
- },
- ],
- },
- }
- );
-
- const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath);
-
- const detailsResultsPolicy = useMemo(() => {
- return [
{
title: i18n.translate('xpack.securitySolution.endpoint.details.policy', {
- defaultMessage: 'Integration Policy',
+ defaultMessage: 'Policy',
}),
description: (
@@ -198,7 +148,7 @@ export const EndpointDetails = memo(
},
{
title: i18n.translate('xpack.securitySolution.endpoint.details.policyStatus', {
- defaultMessage: 'Policy Response',
+ defaultMessage: 'Policy Status',
}),
description: (
// https://github.com/elastic/eui/issues/4530
@@ -210,7 +160,7 @@ export const EndpointDetails = memo(
onClick={policyStatusClickHandler}
onClickAriaLabel={i18n.translate(
'xpack.securitySolution.endpoint.details.policyStatus',
- { defaultMessage: 'Policy Response' }
+ { defaultMessage: 'Policy Status' }
)}
>
@@ -223,10 +173,12 @@ export const EndpointDetails = memo(
),
},
- ];
- }, [details, policyResponseUri, policyStatus, policyStatusClickHandler, policyInfo]);
- const detailsResultsLower = useMemo(() => {
- return [
+ {
+ title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', {
+ defaultMessage: 'Endpoint Version',
+ }),
+ description: {details.agent.version} ,
+ },
{
title: i18n.translate('xpack.securitySolution.endpoint.details.ipAddress', {
defaultMessage: 'IP Address',
@@ -241,70 +193,23 @@ export const EndpointDetails = memo(
),
},
- {
- title: i18n.translate('xpack.securitySolution.endpoint.details.hostname', {
- defaultMessage: 'Hostname',
- }),
- description: {details.host.hostname} ,
- },
- {
- title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', {
- defaultMessage: 'Endpoint Version',
- }),
- description: {details.agent.version} ,
- },
];
- }, [details.agent.version, details.host.hostname, details.host.ip]);
+ }, [
+ details,
+ hostStatus,
+ policyResponseUri,
+ policyStatus,
+ policyStatusClickHandler,
+ policyInfo,
+ ]);
return (
<>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
>
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx
index fccd48cbde67c..d839bbfaae875 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx
@@ -8,7 +8,7 @@
import React, { ComponentType } from 'react';
import moment from 'moment';
-import { EndpointAction, Immutable } from '../../../../../../common/endpoint/types';
+import { ActivityLog, Immutable } from '../../../../../../common/endpoint/types';
import { EndpointDetailsFlyoutTabs } from './components/endpoint_details_tabs';
import { EndpointActivityLog } from './endpoint_activity_log';
import { EndpointDetailsFlyout } from '.';
@@ -17,63 +17,93 @@ import { AsyncResourceState } from '../../../../state';
export const dummyEndpointActivityLog = (
selectedEndpoint: string = ''
-): AsyncResourceState> => ({
+): AsyncResourceState> => ({
type: 'LoadedResourceState',
- data: [
- {
- action_id: '1',
- '@timestamp': moment().subtract(1, 'hours').fromNow().toString(),
- expiration: moment().add(3, 'day').fromNow().toString(),
- type: 'INPUT_ACTION',
- input_type: 'endpoint',
- agents: [`${selectedEndpoint}`],
- user_id: 'sys',
- data: {
- command: 'isolate',
+ data: {
+ total: 20,
+ page: 1,
+ pageSize: 50,
+ data: [
+ {
+ type: 'action',
+ item: {
+ id: '',
+ data: {
+ action_id: '1',
+ '@timestamp': moment().subtract(1, 'hours').fromNow().toString(),
+ expiration: moment().add(3, 'day').fromNow().toString(),
+ type: 'INPUT_ACTION',
+ input_type: 'endpoint',
+ agents: [`${selectedEndpoint}`],
+ user_id: 'sys',
+ data: {
+ command: 'isolate',
+ },
+ },
+ },
},
- },
- {
- action_id: '2',
- '@timestamp': moment().subtract(2, 'hours').fromNow().toString(),
- expiration: moment().add(1, 'day').fromNow().toString(),
- type: 'INPUT_ACTION',
- input_type: 'endpoint',
- agents: [`${selectedEndpoint}`],
- user_id: 'ash',
- data: {
- command: 'isolate',
- comment: 'Sem et tortor consequat id porta nibh venenatis cras sed.',
+ {
+ type: 'action',
+ item: {
+ id: '',
+ data: {
+ action_id: '2',
+ '@timestamp': moment().subtract(2, 'hours').fromNow().toString(),
+ expiration: moment().add(1, 'day').fromNow().toString(),
+ type: 'INPUT_ACTION',
+ input_type: 'endpoint',
+ agents: [`${selectedEndpoint}`],
+ user_id: 'ash',
+ data: {
+ command: 'isolate',
+ comment: 'Sem et tortor consequat id porta nibh venenatis cras sed.',
+ },
+ },
+ },
},
- },
- {
- action_id: '3',
- '@timestamp': moment().subtract(4, 'hours').fromNow().toString(),
- expiration: moment().add(1, 'day').fromNow().toString(),
- type: 'INPUT_ACTION',
- input_type: 'endpoint',
- agents: [`${selectedEndpoint}`],
- user_id: 'someone',
- data: {
- command: 'unisolate',
- comment: 'Turpis egestas pretium aenean pharetra.',
+ {
+ type: 'action',
+ item: {
+ id: '',
+ data: {
+ action_id: '3',
+ '@timestamp': moment().subtract(4, 'hours').fromNow().toString(),
+ expiration: moment().add(1, 'day').fromNow().toString(),
+ type: 'INPUT_ACTION',
+ input_type: 'endpoint',
+ agents: [`${selectedEndpoint}`],
+ user_id: 'someone',
+ data: {
+ command: 'unisolate',
+ comment: 'Turpis egestas pretium aenean pharetra.',
+ },
+ },
+ },
},
- },
- {
- action_id: '4',
- '@timestamp': moment().subtract(1, 'day').fromNow().toString(),
- expiration: moment().add(3, 'day').fromNow().toString(),
- type: 'INPUT_ACTION',
- input_type: 'endpoint',
- agents: [`${selectedEndpoint}`],
- user_id: 'ash',
- data: {
- command: 'isolate',
- comment:
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
- sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ {
+ type: 'action',
+ item: {
+ id: '',
+ data: {
+ action_id: '4',
+ '@timestamp': moment().subtract(1, 'day').fromNow().toString(),
+ expiration: moment().add(3, 'day').fromNow().toString(),
+ type: 'INPUT_ACTION',
+ input_type: 'endpoint',
+ agents: [`${selectedEndpoint}`],
+ user_id: 'ash',
+ data: {
+ command: 'isolate',
+ comment:
+ 'Lorem \
+ ipsum dolor sit amet, consectetur adipiscing elit, \
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ },
+ },
+ },
},
- },
- ],
+ ],
+ },
});
export default {
@@ -100,12 +130,12 @@ export const Tabs = () => (
{
id: 'activity_log',
name: 'Activity Log',
- content: ActivityLog(),
+ content: ActivityLogMarkup(),
},
]}
/>
);
-export const ActivityLog = () => (
-
+export const ActivityLogMarkup = () => (
+
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
index 89c0e3e6a3e06..7892c56fef806 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
@@ -33,7 +33,6 @@ import {
detailsLoading,
getActivityLogData,
getActivityLogError,
- getActivityLogRequestLoading,
showView,
policyResponseConfigurations,
policyResponseActions,
@@ -87,7 +86,6 @@ export const EndpointDetailsFlyout = memo(() => {
} = queryParams;
const activityLog = useEndpointSelector(getActivityLogData);
- const activityLoading = useEndpointSelector(getActivityLogRequestLoading);
const activityError = useEndpointSelector(getActivityLogError);
const hostDetails = useEndpointSelector(detailsData);
const hostDetailsLoading = useEndpointSelector(detailsLoading);
@@ -121,12 +119,8 @@ export const EndpointDetailsFlyout = memo(() => {
},
{
id: EndpointDetailsTabsTypes.activityLog,
- name: i18.ACTIVITY_LOG,
- content: activityLoading ? (
- ContentLoadingMarkup
- ) : (
-
- ),
+ name: i18.ACTIVITY_LOG.tabTitle,
+ content: ,
},
];
@@ -172,14 +166,14 @@ export const EndpointDetailsFlyout = memo(() => {
style={{ zIndex: 4001 }}
data-test-subj="endpointDetailsFlyout"
size="m"
- paddingSize="m"
+ paddingSize="l"
>
-
- {hostDetailsLoading || activityLoading ? (
+
+ {hostDetailsLoading ? (
) : (
-
+
),
},
+ {
+ icon: 'gear',
+ key: 'agentPolicyReassignLink',
+ 'data-test-subj': 'agentPolicyReassignLink',
+ navigateAppId: 'fleet',
+ navigateOptions: {
+ path: `#${
+ pagePathGetters.fleet_agent_details({
+ agentId: fleetAgentId,
+ })[1]
+ }/activity?openReassignFlyout=true`,
+ },
+ href: `${getUrlForApp('fleet')}#${
+ pagePathGetters.fleet_agent_details({
+ agentId: fleetAgentId,
+ })[1]
+ }/activity?openReassignFlyout=true`,
+ children: (
+
+ ),
+ },
];
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index aa1c47a3102d9..86f1e32e751ee 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -516,7 +516,6 @@ describe('when on the endpoint list page', () => {
describe('when there is a selected host in the url', () => {
let hostDetails: HostInfo;
- let elasticAgentId: string;
let renderAndWaitForData: () => Promise>;
const mockEndpointListApi = (mockedPolicyResponse?: HostPolicyResponse) => {
const {
@@ -546,8 +545,6 @@ describe('when on the endpoint list page', () => {
query_strategy_version,
};
- elasticAgentId = hostDetails.metadata.elastic.agent.id;
-
const policy = docGenerator.generatePolicyPackagePolicy();
policy.id = hostDetails.metadata.Endpoint.policy.applied.id;
@@ -738,37 +735,11 @@ describe('when on the endpoint list page', () => {
);
});
- it('should include the link to reassignment in Ingest', async () => {
- coreStart.application.getUrlForApp.mockReturnValue('/app/fleet');
- const renderResult = await renderAndWaitForData();
- const linkToReassign = await renderResult.findByTestId('endpointDetailsLinkToIngest');
- expect(linkToReassign).not.toBeNull();
- expect(linkToReassign.textContent).toEqual('Reassign Policy');
- expect(linkToReassign.getAttribute('href')).toEqual(
- `/app/fleet#/fleet/agents/${elasticAgentId}/activity?openReassignFlyout=true`
- );
- });
-
it('should show the Take Action button', async () => {
const renderResult = await renderAndWaitForData();
expect(renderResult.getByTestId('endpointDetailsActionsButton')).not.toBeNull();
});
- describe('when link to reassignment in Ingest is clicked', () => {
- beforeEach(async () => {
- coreStart.application.getUrlForApp.mockReturnValue('/app/fleet');
- const renderResult = await renderAndWaitForData();
- const linkToReassign = await renderResult.findByTestId('endpointDetailsLinkToIngest');
- reactTestingLibrary.act(() => {
- reactTestingLibrary.fireEvent.click(linkToReassign);
- });
- });
-
- it('should navigate to Ingest without full page refresh', () => {
- expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1);
- });
- });
-
describe('when showing host Policy Response panel', () => {
let renderResult: ReturnType;
beforeEach(async () => {
@@ -1139,5 +1110,12 @@ describe('when on the endpoint list page', () => {
const agentDetailsLink = await renderResult.findByTestId('agentDetailsLink');
expect(agentDetailsLink.getAttribute('href')).toEqual(`/app/fleet#/fleet/agents/${agentId}`);
});
+
+ it('navigates to the Ingest Agent Details page with policy reassign', async () => {
+ const agentPolicyReassignLink = await renderResult.findByTestId('agentPolicyReassignLink');
+ expect(agentPolicyReassignLink.getAttribute('href')).toEqual(
+ `/app/fleet#/fleet/agents/${agentId}/activity?openReassignFlyout=true`
+ );
+ });
});
});
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 4d1ab0f3de825..410afb4684cd5 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
@@ -31,11 +31,7 @@ import { EndpointDetailsFlyout } from './details';
import * as selectors from '../store/selectors';
import { useEndpointSelector } from './hooks';
import { isPolicyOutOfDate } from '../utils';
-import {
- HOST_STATUS_TO_BADGE_COLOR,
- POLICY_STATUS_TO_BADGE_COLOR,
- POLICY_STATUS_TO_TEXT,
-} from './host_constants';
+import { POLICY_STATUS_TO_BADGE_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants';
import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { CreateStructuredSelector } from '../../../../common/store';
import { Immutable, HostInfo } from '../../../../../common/endpoint/types';
@@ -59,6 +55,7 @@ import { AdministrationListPage } from '../../../components/administration_list_
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { LinkToApp } from '../../../../common/components/endpoint/link_to_app';
import { TableRowActions } from './components/table_row_actions';
+import { EndpointAgentStatus } from './components/endpoint_agent_status';
const MAX_PAGINATED_ITEM = 9999;
@@ -97,6 +94,7 @@ const EndpointListNavLink = memo<{
});
EndpointListNavLink.displayName = 'EndpointListNavLink';
+// FIXME: this needs refactoring - we are pulling in all selectors from endpoint, which includes many more than what the list uses
const selector = (createStructuredSelector as CreateStructuredSelector)(selectors);
export const EndpointList = () => {
const history = useHistory();
@@ -279,19 +277,9 @@ export const EndpointList = () => {
defaultMessage: 'Agent Status',
}),
// eslint-disable-next-line react/display-name
- render: (hostStatus: HostInfo['host_status']) => {
+ render: (hostStatus: HostInfo['host_status'], endpointInfo) => {
return (
-
-
-
+
);
},
},
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
index fd2806713183b..1a7889f22db16 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
@@ -11,9 +11,53 @@ export const OVERVIEW = i18n.translate('xpack.securitySolution.endpointDetails.o
defaultMessage: 'Overview',
});
-export const ACTIVITY_LOG = i18n.translate('xpack.securitySolution.endpointDetails.activityLog', {
- defaultMessage: 'Activity Log',
-});
+export const ACTIVITY_LOG = {
+ tabTitle: i18n.translate('xpack.securitySolution.endpointDetails.activityLog', {
+ defaultMessage: 'Activity Log',
+ }),
+ LogEntry: {
+ action: {
+ isolatedAction: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.isolated',
+ {
+ defaultMessage: 'isolated host',
+ }
+ ),
+ unisolatedAction: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.unisolated',
+ {
+ defaultMessage: 'unisolated host',
+ }
+ ),
+ },
+ response: {
+ isolationSuccessful: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationSuccessful',
+ {
+ defaultMessage: 'host isolation successful',
+ }
+ ),
+ isolationFailed: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationFailed',
+ {
+ defaultMessage: 'host isolation failed',
+ }
+ ),
+ unisolationSuccessful: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationSuccessful',
+ {
+ defaultMessage: 'host unisolation successful',
+ }
+ ),
+ unisolationFailed: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationFailed',
+ {
+ defaultMessage: 'host unisolation failed',
+ }
+ ),
+ },
+ },
+};
export const SEARCH_ACTIVITY_LOG = i18n.translate(
'xpack.securitySolution.endpointDetails.activityLog.search',
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts
index d4e81fd812668..fef6ccb99a17a 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts
@@ -59,6 +59,13 @@ export const getListItems: EventFiltersSelector<
return apiResponseData?.data || [];
});
+export const getTotalCountListItems: EventFiltersSelector> = createSelector(
+ getListApiSuccessResponse,
+ (apiResponseData) => {
+ return apiResponseData?.total || 0;
+ }
+);
+
/**
* Will return the query that was used with the currently displayed list of content. If a new page
* of content is being loaded, this selector will then attempt to use the previousState to return
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts
index ac2b16e51603c..9d2d3c394c416 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts
@@ -17,6 +17,7 @@ import {
getCurrentListPageDataState,
getListApiSuccessResponse,
getListItems,
+ getTotalCountListItems,
getCurrentListItemsQuery,
getListPagination,
getListFetchError,
@@ -120,6 +121,19 @@ describe('event filters selectors', () => {
});
});
+ describe('getTotalCountListItems()', () => {
+ it('should return the list items from api response', () => {
+ setToLoadedState();
+ expect(getTotalCountListItems(initialState)).toEqual(
+ getLastLoadedResourceState(initialState.listPage.data)?.data.content.total
+ );
+ });
+
+ it('should return empty array if no api response', () => {
+ expect(getTotalCountListItems(initialState)).toEqual(0);
+ });
+ });
+
describe('getCurrentListItemsQuery()', () => {
it('should return empty object if Uninitialized', () => {
expect(getCurrentListItemsQuery(initialState)).toEqual({});
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.tsx
index 74a023965a57d..653469d304978 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.tsx
@@ -91,7 +91,7 @@ export const EventFilterDeleteModal = memo<{}>(() => {
{eventFilter?.name} }}
+ values={{ name: {eventFilter?.name} }}
/>
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
index 00ee80c5d7022..0975104f02297 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
@@ -32,6 +32,7 @@ import {
getActionError,
getFormEntry,
showDeleteModal,
+ getTotalCountListItems,
} from '../store/selector';
import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content';
import { Immutable, ListPageRouteState } from '../../../../../common/endpoint/types';
@@ -66,6 +67,7 @@ export const EventFiltersListPage = memo(() => {
const isActionError = useEventFiltersSelector(getActionError);
const formEntry = useEventFiltersSelector(getFormEntry);
const listItems = useEventFiltersSelector(getListItems);
+ const totalCountListItems = useEventFiltersSelector(getTotalCountListItems);
const pagination = useEventFiltersSelector(getListPagination);
const isLoading = useEventFiltersSelector(getListIsLoading);
const fetchError = useEventFiltersSelector(getListFetchError);
@@ -235,7 +237,7 @@ export const EventFiltersListPage = memo(() => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx
index be3cba5eb4318..5588cdbe81e3e 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx
@@ -20,7 +20,7 @@ import {
GetExceptionSummaryResponse,
ListPageRouteState,
} from '../../../../../../../../common/endpoint/types';
-import { PLUGIN_ID as FLEET_PLUGIN_ID } from '../../../../../../../../../fleet/common';
+import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common';
import { MANAGEMENT_APP_ID } from '../../../../../../common/constants';
import { useToasts } from '../../../../../../../common/lib/kibana';
import { LinkWithIcon } from './link_with_icon';
@@ -68,19 +68,21 @@ export const FleetEventFiltersCard = memo(
}, [eventFiltersApi, toasts]);
const eventFiltersRouteState = useMemo(() => {
- const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details_custom({ pkgkey })}`;
+ const fleetPackageCustomUrlPath = `#${
+ pagePathGetters.integration_details_custom({ pkgkey })[1]
+ }`;
return {
backButtonLabel: i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel',
{ defaultMessage: 'Back to Endpoint Integration' }
),
onBackButtonNavigateTo: [
- FLEET_PLUGIN_ID,
+ INTEGRATIONS_PLUGIN_ID,
{
path: fleetPackageCustomUrlPath,
},
],
- backButtonUrl: getUrlForApp(FLEET_PLUGIN_ID, {
+ backButtonUrl: getUrlForApp(INTEGRATIONS_PLUGIN_ID, {
path: fleetPackageCustomUrlPath,
}),
};
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx
index ed3ba10c1e62b..f1c9cb13a27dc 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx
@@ -20,7 +20,7 @@ import {
ListPageRouteState,
GetExceptionSummaryResponse,
} from '../../../../../../../../common/endpoint/types';
-import { PLUGIN_ID as FLEET_PLUGIN_ID } from '../../../../../../../../../fleet/common';
+import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common';
import { MANAGEMENT_APP_ID } from '../../../../../../common/constants';
import { useToasts } from '../../../../../../../common/lib/kibana';
import { LinkWithIcon } from './link_with_icon';
@@ -68,24 +68,26 @@ export const FleetTrustedAppsCard = memo((
const trustedAppsListUrlPath = getTrustedAppsListPath();
const trustedAppRouteState = useMemo(() => {
- const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details_custom({ pkgkey })}`;
+ const fleetPackageCustomUrlPath = `#${
+ pagePathGetters.integration_details_custom({ pkgkey })[1]
+ }`;
+
return {
backButtonLabel: i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel',
{ defaultMessage: 'Back to Endpoint Integration' }
),
onBackButtonNavigateTo: [
- FLEET_PLUGIN_ID,
+ INTEGRATIONS_PLUGIN_ID,
{
path: fleetPackageCustomUrlPath,
},
],
- backButtonUrl: getUrlForApp(FLEET_PLUGIN_ID, {
+ backButtonUrl: getUrlForApp(INTEGRATIONS_PLUGIN_ID, {
path: fleetPackageCustomUrlPath,
}),
};
}, [getUrlForApp, pkgkey]);
-
return (
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap
index 5ab58914ff8b1..0343ab62b9773 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap
@@ -56,9 +56,11 @@ exports[`TrustedAppDeletionDialog renders correctly when deletion failed 1`] = `
>
You are removing trusted application "
-
+
trusted app 3
-
+
".
@@ -158,9 +160,11 @@ exports[`TrustedAppDeletionDialog renders correctly when deletion is in progress
>
You are removing trusted application "
-
+
trusted app 3
-
+
".
@@ -265,9 +269,11 @@ exports[`TrustedAppDeletionDialog renders correctly when dialog started 1`] = `
>
You are removing trusted application "
-
+
trusted app 3
-
+
".
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx
index bffd980610372..3afa2642eba12 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx
@@ -45,7 +45,7 @@ const getTranslations = (entry: Immutable | undefined) => ({
{entry?.name} }}
+ values={{ name: {entry?.name} }}
/>
),
subMessage: (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
index da45579b34773..dd8cdb818cad7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
@@ -52,6 +52,7 @@ import * as i18n from './translations';
import * as commonI18n from '../../timeline/properties/translations';
import { getTimelineStatusByIdSelector } from './selectors';
import { TimelineKPIs } from './kpis';
+import { LineClamp } from '../../../../common/components/line_clamp';
// to hide side borders
const StyledPanel = styled(EuiPanel)`
@@ -206,13 +207,13 @@ const TimelineDescriptionComponent: React.FC = ({ timelineId
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).description
);
- const content = useMemo(() => (description.length ? description : commonI18n.DESCRIPTION), [
- description,
- ]);
-
return (
- {content}
+ {description.length ? (
+
+ ) : (
+ commonI18n.DESCRIPTION
+ )}
);
};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx
index 98d678a25b4c6..65963c9609320 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx
@@ -10,7 +10,7 @@
import { EuiButtonIcon, EuiLink } from '@elastic/eui';
import { omit } from 'lodash/fp';
import React from 'react';
-
+import styled from 'styled-components';
import { ACTION_COLUMN_WIDTH } from './common_styles';
import { isUntitled } from '../helpers';
import { NotePreviews } from '../note_previews';
@@ -20,6 +20,14 @@ import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
import { TimelineType } from '../../../../../common/types/timeline';
+const DescriptionCell = styled.span`
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 5;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+`;
+
/**
* Returns the column definitions (passed as the `columns` prop to
* `EuiBasicTable`) that are common to the compact `Open Timeline` modal view,
@@ -85,9 +93,9 @@ export const getCommonColumns = ({
field: 'description',
name: i18n.DESCRIPTION,
render: (description: string) => (
-
+
{description != null && description.trim().length > 0 ? description : getEmptyTagValue()}
-
+
),
sortable: false,
},
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts
new file mode 100644
index 0000000000000..9b73721775382
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts
@@ -0,0 +1,186 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { KibanaResponseFactory, RequestHandler, RouteConfig } from 'kibana/server';
+import {
+ elasticsearchServiceMock,
+ httpServerMock,
+ httpServiceMock,
+ loggingSystemMock,
+ savedObjectsClientMock,
+} from 'src/core/server/mocks';
+import {
+ EndpointActionLogRequestParams,
+ EndpointActionLogRequestQuery,
+ EndpointActionLogRequestSchema,
+} from '../../../../common/endpoint/schema/actions';
+import { ENDPOINT_ACTION_LOG_ROUTE } from '../../../../common/endpoint/constants';
+import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
+import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
+import { EndpointAppContextService } from '../../endpoint_app_context_services';
+import {
+ createMockEndpointAppContextServiceStartContract,
+ createRouteHandlerContext,
+} from '../../mocks';
+import { registerActionAuditLogRoutes } from './audit_log';
+import uuid from 'uuid';
+import { aMockAction, aMockResponse, MockAction, mockAuditLog, MockResponse } from './mocks';
+import { SecuritySolutionRequestHandlerContext } from '../../../types';
+import { ActivityLog } from '../../../../common/endpoint/types';
+
+describe('Action Log API', () => {
+ describe('schema', () => {
+ it('should require at least 1 agent ID', () => {
+ expect(() => {
+ EndpointActionLogRequestSchema.params.validate({}); // no agent_ids provided
+ }).toThrow();
+ });
+
+ it('should accept a single agent ID', () => {
+ expect(() => {
+ EndpointActionLogRequestSchema.params.validate({ agent_id: uuid.v4() });
+ }).not.toThrow();
+ });
+
+ it('should work without query params', () => {
+ expect(() => {
+ EndpointActionLogRequestSchema.query.validate({});
+ }).not.toThrow();
+ });
+
+ it('should work with query params', () => {
+ expect(() => {
+ EndpointActionLogRequestSchema.query.validate({ page: 10, page_size: 100 });
+ }).not.toThrow();
+ });
+
+ it('should not work without allowed page and page_size params', () => {
+ expect(() => {
+ EndpointActionLogRequestSchema.query.validate({ page_size: 101 });
+ }).toThrow();
+ });
+ });
+
+ describe('response', () => {
+ const mockID = 'XYZABC-000';
+ const actionID = 'some-known-actionid';
+ let endpointAppContextService: EndpointAppContextService;
+
+ // convenience for calling the route and handler for audit log
+ let getActivityLog: (
+ query?: EndpointActionLogRequestQuery
+ ) => Promise>;
+ // convenience for injecting mock responses for actions index and responses
+ let havingActionsAndResponses: (actions: MockAction[], responses: any[]) => void;
+
+ let havingErrors: () => void;
+
+ beforeEach(() => {
+ const esClientMock = elasticsearchServiceMock.createScopedClusterClient();
+ const routerMock = httpServiceMock.createRouter();
+ endpointAppContextService = new EndpointAppContextService();
+ endpointAppContextService.start(createMockEndpointAppContextServiceStartContract());
+
+ registerActionAuditLogRoutes(routerMock, {
+ logFactory: loggingSystemMock.create(),
+ service: endpointAppContextService,
+ config: () => Promise.resolve(createMockConfig()),
+ experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
+ });
+
+ getActivityLog = async (query?: any): Promise> => {
+ const req = httpServerMock.createKibanaRequest({
+ params: { agent_id: mockID },
+ query,
+ });
+ const mockResponse = httpServerMock.createResponseFactory();
+ const [, routeHandler]: [
+ RouteConfig,
+ RequestHandler<
+ EndpointActionLogRequestParams,
+ EndpointActionLogRequestQuery,
+ unknown,
+ SecuritySolutionRequestHandlerContext
+ >
+ ] = routerMock.get.mock.calls.find(([{ path }]) =>
+ path.startsWith(ENDPOINT_ACTION_LOG_ROUTE)
+ )!;
+ await routeHandler(
+ createRouteHandlerContext(esClientMock, savedObjectsClientMock.create()),
+ req,
+ mockResponse
+ );
+
+ return mockResponse;
+ };
+
+ havingActionsAndResponses = (actions: MockAction[], responses: MockResponse[]) => {
+ const actionsData = actions.map((a) => ({
+ _index: '.fleet-actions-7',
+ _source: a.build(),
+ }));
+ const responsesData = responses.map((r) => ({
+ _index: '.ds-.fleet-actions-results-2021.06.09-000001',
+ _source: r.build(),
+ }));
+ const mockResult = mockAuditLog([...actionsData, ...responsesData]);
+ esClientMock.asCurrentUser.search = jest
+ .fn()
+ .mockImplementationOnce(() => Promise.resolve(mockResult));
+ };
+
+ havingErrors = () => {
+ esClientMock.asCurrentUser.search = jest.fn().mockImplementationOnce(() =>
+ Promise.resolve(() => {
+ throw new Error();
+ })
+ );
+ };
+ });
+
+ afterEach(() => {
+ endpointAppContextService.stop();
+ });
+
+ it('should return an empty array when nothing in audit log', async () => {
+ havingActionsAndResponses([], []);
+ const response = await getActivityLog();
+ expect(response.ok).toBeCalled();
+ expect((response.ok.mock.calls[0][0]?.body as ActivityLog).data).toHaveLength(0);
+ });
+
+ it('should have actions and action responses', async () => {
+ havingActionsAndResponses(
+ [
+ aMockAction().withAgent(mockID).withAction('isolate'),
+ aMockAction().withAgent(mockID).withAction('unisolate'),
+ aMockAction().withAgent(mockID).withAction('isolate'),
+ ],
+ [aMockResponse(actionID, mockID), aMockResponse(actionID, mockID)]
+ );
+ const response = await getActivityLog();
+ const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog;
+
+ expect(response.ok).toBeCalled();
+ expect(responseBody.data).toHaveLength(5);
+ expect(responseBody.data.filter((x: any) => x.type === 'response')).toHaveLength(2);
+ expect(responseBody.data.filter((x: any) => x.type === 'action')).toHaveLength(3);
+ });
+
+ it('should throw errors when no results for some agentID', async () => {
+ havingErrors();
+
+ try {
+ await getActivityLog();
+ } catch (error) {
+ expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockID}`);
+ }
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts
index fdbb9608463e9..b0cea299af60d 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts
@@ -5,55 +5,34 @@
* 2.0.
*/
-import { TypeOf } from '@kbn/config-schema';
import { RequestHandler } from 'kibana/server';
-import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common';
-import { EndpointActionLogRequestSchema } from '../../../../common/endpoint/schema/actions';
-
+import {
+ EndpointActionLogRequestParams,
+ EndpointActionLogRequestQuery,
+} from '../../../../common/endpoint/schema/actions';
+import { getAuditLogResponse } from './service';
import { SecuritySolutionRequestHandlerContext } from '../../../types';
import { EndpointAppContext } from '../../types';
export const actionsLogRequestHandler = (
endpointContext: EndpointAppContext
): RequestHandler<
- TypeOf,
- unknown,
+ EndpointActionLogRequestParams,
+ EndpointActionLogRequestQuery,
unknown,
SecuritySolutionRequestHandlerContext
> => {
const logger = endpointContext.logFactory.get('audit_log');
+
return async (context, req, res) => {
- const esClient = context.core.elasticsearch.client.asCurrentUser;
- let result;
- try {
- result = await esClient.search({
- index: AGENT_ACTIONS_INDEX,
- body: {
- query: {
- match: {
- agents: req.params.agent_id,
- },
- },
- sort: [
- {
- '@timestamp': {
- order: 'desc',
- },
- },
- ],
- },
- });
- } catch (error) {
- logger.error(error);
- throw error;
- }
- if (result?.statusCode !== 200) {
- logger.error(`Error fetching actions log for agent_id ${req.params.agent_id}`);
- throw new Error(`Error fetching actions log for agent_id ${req.params.agent_id}`);
- }
+ const {
+ params: { agent_id: elasticAgentId },
+ query: { page, page_size: pageSize },
+ } = req;
+ const body = await getAuditLogResponse({ elasticAgentId, page, pageSize, context, logger });
return res.ok({
- body: result.body.hits.hits.map((e) => e._source),
+ body,
});
};
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts
index 34f7d140a78de..f74ae07fdfac4 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts
@@ -18,6 +18,29 @@ import {
ISOLATION_ACTIONS,
} from '../../../../common/endpoint/types';
+export const mockAuditLog = (results: any = []): ApiResponse => {
+ return {
+ body: {
+ hits: {
+ total: results.length,
+ hits: results.map((a: any) => {
+ const _index = a._index;
+ delete a._index;
+ const _source = a;
+ return {
+ _index,
+ _source,
+ };
+ }),
+ },
+ },
+ statusCode: 200,
+ headers: {},
+ warnings: [],
+ meta: {} as any,
+ };
+};
+
export const mockSearchResult = (results: any = []): ApiResponse => {
return {
body: {
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts
new file mode 100644
index 0000000000000..20b29694a1df1
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts
@@ -0,0 +1,110 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Logger } from 'kibana/server';
+import type { estypes } from '@elastic/elasticsearch';
+import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../../fleet/common';
+import { SecuritySolutionRequestHandlerContext } from '../../../types';
+
+export const getAuditLogESQuery = ({
+ elasticAgentId,
+ from,
+ size,
+}: {
+ elasticAgentId: string;
+ from: number;
+ size: number;
+}): estypes.SearchRequest => {
+ return {
+ index: [AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX],
+ size,
+ from,
+ body: {
+ query: {
+ bool: {
+ should: [
+ { terms: { agents: [elasticAgentId] } },
+ { terms: { agent_id: [elasticAgentId] } },
+ ],
+ },
+ },
+ sort: [
+ {
+ '@timestamp': {
+ order: 'desc',
+ },
+ },
+ ],
+ },
+ };
+};
+
+export const getAuditLogResponse = async ({
+ elasticAgentId,
+ page,
+ pageSize,
+ context,
+ logger,
+}: {
+ elasticAgentId: string;
+ page: number;
+ pageSize: number;
+ context: SecuritySolutionRequestHandlerContext;
+ logger: Logger;
+}): Promise<{
+ total: number;
+ page: number;
+ pageSize: number;
+ data: Array<{
+ type: 'action' | 'response';
+ item: {
+ id: string;
+ data: unknown;
+ };
+ }>;
+}> => {
+ const size = pageSize;
+ const from = page <= 1 ? 0 : page * pageSize - pageSize + 1;
+
+ const options = {
+ headers: {
+ 'X-elastic-product-origin': 'fleet',
+ },
+ ignore: [404],
+ };
+ const esClient = context.core.elasticsearch.client.asCurrentUser;
+ let result;
+ const params = getAuditLogESQuery({
+ elasticAgentId,
+ from,
+ size,
+ });
+
+ try {
+ result = await esClient.search(params, options);
+ } catch (error) {
+ logger.error(error);
+ throw error;
+ }
+ if (result?.statusCode !== 200) {
+ logger.error(`Error fetching actions log for agent_id ${elasticAgentId}`);
+ throw new Error(`Error fetching actions log for agent_id ${elasticAgentId}`);
+ }
+
+ return {
+ total:
+ typeof result.body.hits.total === 'number'
+ ? result.body.hits.total
+ : result.body.hits.total.value,
+ page,
+ pageSize,
+ data: result.body.hits.hits.map((e) => ({
+ type: e._index.startsWith('.fleet-actions') ? 'action' : 'response',
+ item: { id: e._id, data: e._source },
+ })),
+ };
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts
index 6f8dac5b49b31..a4863e577c6bc 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts
@@ -91,7 +91,6 @@ export const rulesNotificationAlertType = ({
signalsCount,
resultsLink,
ruleParams,
- // @ts-expect-error @elastic/elasticsearch _source is optional
signals,
});
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts
index e7db10380eea1..bfb96e97edf11 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts
@@ -8,7 +8,6 @@
import { mapKeys, snakeCase } from 'lodash/fp';
import { AlertInstance } from '../../../../../alerting/server';
import { RuleParams } from '../schemas/rule_schemas';
-import { SignalSource } from '../signals/types';
export type NotificationRuleTypeParams = RuleParams & {
name: string;
@@ -20,7 +19,7 @@ interface ScheduleNotificationActions {
signalsCount: number;
resultsLink: string;
ruleParams: NotificationRuleTypeParams;
- signals: SignalSource[];
+ signals: unknown[];
}
export const scheduleNotificationActions = ({
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/build_rule_message.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/build_rule_message.mock.ts
new file mode 100644
index 0000000000000..f43142d1d0264
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/build_rule_message.mock.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { buildRuleMessageFactory } from '../rule_messages';
+
+export const mockBuildRuleMessage = buildRuleMessageFactory({
+ id: 'fake id',
+ ruleId: 'fake rule id',
+ index: 'fakeindex',
+ name: 'fake name',
+});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_factory.ts
new file mode 100644
index 0000000000000..f518ac2386d0b
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_factory.ts
@@ -0,0 +1,100 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { performance } from 'perf_hooks';
+import { countBy, isEmpty, get } from 'lodash';
+import { ElasticsearchClient, Logger } from 'kibana/server';
+import { BuildRuleMessage } from './rule_messages';
+import { RefreshTypes } from '../types';
+import { BaseHit } from '../../../../common/detection_engine/types';
+import { errorAggregator, makeFloatString } from './utils';
+
+export interface GenericBulkCreateResponse {
+ success: boolean;
+ bulkCreateDuration: string;
+ createdItemsCount: number;
+ createdItems: Array;
+ errors: string[];
+}
+
+export const bulkCreateFactory = (
+ logger: Logger,
+ esClient: ElasticsearchClient,
+ buildRuleMessage: BuildRuleMessage,
+ refreshForBulkCreate: RefreshTypes
+) => async (wrappedDocs: Array>): Promise> => {
+ if (wrappedDocs.length === 0) {
+ return {
+ errors: [],
+ success: true,
+ bulkCreateDuration: '0',
+ createdItemsCount: 0,
+ createdItems: [],
+ };
+ }
+
+ const bulkBody = wrappedDocs.flatMap((wrappedDoc) => [
+ {
+ create: {
+ _index: wrappedDoc._index,
+ _id: wrappedDoc._id,
+ },
+ },
+ wrappedDoc._source,
+ ]);
+ const start = performance.now();
+
+ const { body: response } = await esClient.bulk({
+ refresh: refreshForBulkCreate,
+ body: bulkBody,
+ });
+
+ const end = performance.now();
+ logger.debug(
+ buildRuleMessage(
+ `individual bulk process time took: ${makeFloatString(end - start)} milliseconds`
+ )
+ );
+ logger.debug(buildRuleMessage(`took property says bulk took: ${response.took} milliseconds`));
+ const createdItems = wrappedDocs
+ .map((doc, index) => ({
+ _id: response.items[index].create?._id ?? '',
+ _index: response.items[index].create?._index ?? '',
+ ...doc._source,
+ }))
+ .filter((_, index) => get(response.items[index], 'create.status') === 201);
+ const createdItemsCount = createdItems.length;
+ const duplicateSignalsCount = countBy(response.items, 'create.status')['409'];
+ const errorCountByMessage = errorAggregator(response, [409]);
+
+ logger.debug(buildRuleMessage(`bulk created ${createdItemsCount} signals`));
+ if (duplicateSignalsCount > 0) {
+ logger.debug(buildRuleMessage(`ignored ${duplicateSignalsCount} duplicate signals`));
+ }
+ if (!isEmpty(errorCountByMessage)) {
+ logger.error(
+ buildRuleMessage(
+ `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}`
+ )
+ );
+ return {
+ errors: Object.keys(errorCountByMessage),
+ success: false,
+ bulkCreateDuration: makeFloatString(end - start),
+ createdItemsCount,
+ createdItems,
+ };
+ } else {
+ return {
+ errors: [],
+ success: true,
+ bulkCreateDuration: makeFloatString(end - start),
+ createdItemsCount,
+ createdItems,
+ };
+ }
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts
index 00ac40fa7e27c..ebb4462817eab 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts
@@ -14,11 +14,10 @@ import {
AlertInstanceState,
AlertServices,
} from '../../../../../alerting/server';
-import { RefreshTypes } from '../types';
-import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create';
+import { GenericBulkCreateResponse } from './bulk_create_factory';
import { AnomalyResults, Anomaly } from '../../machine_learning';
import { BuildRuleMessage } from './rule_messages';
-import { AlertAttributes } from './types';
+import { AlertAttributes, BulkCreate, WrapHits } from './types';
import { MachineLearningRuleParams } from '../schemas/rule_schemas';
interface BulkCreateMlSignalsParams {
@@ -28,8 +27,9 @@ interface BulkCreateMlSignalsParams {
logger: Logger;
id: string;
signalsIndex: string;
- refresh: RefreshTypes;
buildRuleMessage: BuildRuleMessage;
+ bulkCreate: BulkCreate;
+ wrapHits: WrapHits;
}
interface EcsAnomaly extends Anomaly {
@@ -85,9 +85,10 @@ const transformAnomalyResultsToEcs = (
export const bulkCreateMlSignals = async (
params: BulkCreateMlSignalsParams
-): Promise => {
+): Promise> => {
const anomalyResults = params.someResult;
const ecsResults = transformAnomalyResultsToEcs(anomalyResults);
- const buildRuleMessage = params.buildRuleMessage;
- return singleBulkCreate({ ...params, filteredEvents: ecsResults, buildRuleMessage });
+
+ const wrappedDocs = params.wrapHits(ecsResults.hits.hits);
+ return params.bulkCreate(wrappedDocs);
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts
index f8f77bd2bf6e6..947e7d573173e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts
@@ -7,7 +7,6 @@
import { loggingSystemMock } from 'src/core/server/mocks';
import { alertsMock, AlertServicesMock } from '../../../../../../alerting/server/mocks';
-import { RuleStatusService } from '../rule_status_service';
import { eqlExecutor } from './eql';
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getEntryListMock } from '../../../../../../lists/common/schemas/types/entry_list.mock';
@@ -23,7 +22,6 @@ describe('eql_executor', () => {
const version = '8.0.0';
let logger: ReturnType;
let alertServices: AlertServicesMock;
- let ruleStatusService: Record;
(getIndexVersion as jest.Mock).mockReturnValue(SIGNALS_TEMPLATE_VERSION);
const eqlSO = {
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
@@ -51,17 +49,11 @@ describe('eql_executor', () => {
beforeEach(() => {
alertServices = alertsMock.createAlertServices();
logger = loggingSystemMock.createLogger();
- ruleStatusService = {
- success: jest.fn(),
- find: jest.fn(),
- goingToRun: jest.fn(),
- error: jest.fn(),
- partialFailure: jest.fn(),
- };
alertServices.scopedClusterClient.asCurrentUser.transport.request.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise({
hits: {
total: { value: 10 },
+ events: [],
},
})
);
@@ -70,25 +62,16 @@ describe('eql_executor', () => {
describe('eqlExecutor', () => {
it('should set a warning when exception list for eql rule contains value list exceptions', async () => {
const exceptionItems = [getExceptionListItemSchemaMock({ entries: [getEntryListMock()] })];
- try {
- await eqlExecutor({
- rule: eqlSO,
- exceptionItems,
- ruleStatusService: (ruleStatusService as unknown) as RuleStatusService,
- services: alertServices,
- version,
- logger,
- refresh: false,
- searchAfterSize,
- });
- } catch (err) {
- // eqlExecutor will throw until we have an EQL response mock that conforms to the
- // expected EQL response format, so just catch the error and check the status service
- }
- expect(ruleStatusService.partialFailure).toHaveBeenCalled();
- expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain(
- 'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules'
- );
+ const response = await eqlExecutor({
+ rule: eqlSO,
+ exceptionItems,
+ services: alertServices,
+ version,
+ logger,
+ searchAfterSize,
+ bulkCreate: jest.fn(),
+ });
+ expect(response.warningMessages.length).toEqual(1);
});
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts
index 8e2f5e92ae502..28d1f3e19baee 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts
@@ -7,9 +7,9 @@
import { ApiResponse } from '@elastic/elasticsearch';
import { performance } from 'perf_hooks';
-import { Logger } from 'src/core/server';
import { SavedObject } from 'src/core/types';
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
+import { Logger } from 'src/core/server';
import {
AlertInstanceContext,
AlertInstanceState,
@@ -21,13 +21,12 @@ import { isOutdated } from '../../migrations/helpers';
import { getIndexVersion } from '../../routes/index/get_index_version';
import { MIN_EQL_RULE_INDEX_VERSION } from '../../routes/index/get_signals_template';
import { EqlRuleParams } from '../../schemas/rule_schemas';
-import { RefreshTypes } from '../../types';
import { buildSignalFromEvent, buildSignalGroupFromSequence } from '../build_bulk_body';
import { getInputIndex } from '../get_input_output_index';
-import { RuleStatusService } from '../rule_status_service';
-import { bulkInsertSignals, filterDuplicateSignals } from '../single_bulk_create';
+import { filterDuplicateSignals } from '../filter_duplicate_signals';
import {
AlertAttributes,
+ BulkCreate,
EqlSignalSearchResponse,
SearchAfterAndBulkCreateReturnType,
WrappedSignalHit,
@@ -37,26 +36,24 @@ import { createSearchAfterReturnType, makeFloatString, wrapSignal } from '../uti
export const eqlExecutor = async ({
rule,
exceptionItems,
- ruleStatusService,
services,
version,
- searchAfterSize,
logger,
- refresh,
+ searchAfterSize,
+ bulkCreate,
}: {
rule: SavedObject>;
exceptionItems: ExceptionListItemSchema[];
- ruleStatusService: RuleStatusService;
services: AlertServices;
version: string;
- searchAfterSize: number;
logger: Logger;
- refresh: RefreshTypes;
+ searchAfterSize: number;
+ bulkCreate: BulkCreate;
}): Promise => {
const result = createSearchAfterReturnType();
const ruleParams = rule.attributes.params;
if (hasLargeValueItem(exceptionItems)) {
- await ruleStatusService.partialFailure(
+ result.warningMessages.push(
'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules'
);
result.warning = true;
@@ -125,7 +122,7 @@ export const eqlExecutor = async ({
}
if (newSignals.length > 0) {
- const insertResult = await bulkInsertSignals(newSignals, logger, services, refresh);
+ const insertResult = await bulkCreate(newSignals);
result.bulkCreateTimes.push(insertResult.bulkCreateDuration);
result.createdSignalsCount += insertResult.createdItemsCount;
result.createdSignals = insertResult.createdItems;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts
index e157750a7d51b..25a9d2c3f510f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts
@@ -7,7 +7,6 @@
import { loggingSystemMock } from 'src/core/server/mocks';
import { alertsMock, AlertServicesMock } from '../../../../../../alerting/server/mocks';
-import { RuleStatusService } from '../rule_status_service';
import { mlExecutor } from './ml';
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getMlRuleParams } from '../../schemas/rule_schemas.mock';
@@ -17,7 +16,6 @@ import { findMlSignals } from '../find_ml_signals';
import { bulkCreateMlSignals } from '../bulk_create_ml_signals';
import { mlPluginServerMock } from '../../../../../../ml/server/mocks';
import { sampleRuleSO } from '../__mocks__/es_results';
-import { getRuleStatusServiceMock } from '../rule_status_service.mock';
jest.mock('../find_ml_signals');
jest.mock('../bulk_create_ml_signals');
@@ -25,7 +23,6 @@ jest.mock('../bulk_create_ml_signals');
describe('ml_executor', () => {
let jobsSummaryMock: jest.Mock;
let mlMock: ReturnType;
- let ruleStatusService: ReturnType;
const exceptionItems = [getExceptionListItemSchemaMock()];
let logger: ReturnType;
let alertServices: AlertServicesMock;
@@ -45,7 +42,6 @@ describe('ml_executor', () => {
mlMock.jobServiceProvider.mockReturnValue({
jobsSummary: jobsSummaryMock,
});
- ruleStatusService = getRuleStatusServiceMock();
(findMlSignals as jest.Mock).mockResolvedValue({
_shards: {},
hits: {
@@ -66,35 +62,32 @@ describe('ml_executor', () => {
rule: mlSO,
ml: undefined,
exceptionItems,
- ruleStatusService,
services: alertServices,
logger,
- refresh: false,
buildRuleMessage,
listClient: getListClientMock(),
+ bulkCreate: jest.fn(),
+ wrapHits: jest.fn(),
})
).rejects.toThrow('ML plugin unavailable during rule execution');
});
it('should record a partial failure if Machine learning job summary was null', async () => {
jobsSummaryMock.mockResolvedValue([]);
- await mlExecutor({
+ const response = await mlExecutor({
rule: mlSO,
ml: mlMock,
exceptionItems,
- ruleStatusService,
services: alertServices,
logger,
- refresh: false,
buildRuleMessage,
listClient: getListClientMock(),
+ bulkCreate: jest.fn(),
+ wrapHits: jest.fn(),
});
expect(logger.warn).toHaveBeenCalled();
expect(logger.warn.mock.calls[0][0]).toContain('Machine learning job(s) are not started');
- expect(ruleStatusService.partialFailure).toHaveBeenCalled();
- expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain(
- 'Machine learning job(s) are not started'
- );
+ expect(response.warningMessages.length).toEqual(1);
});
it('should record a partial failure if Machine learning job was not started', async () => {
@@ -106,22 +99,19 @@ describe('ml_executor', () => {
},
]);
- await mlExecutor({
+ const response = await mlExecutor({
rule: mlSO,
ml: mlMock,
exceptionItems,
- ruleStatusService: (ruleStatusService as unknown) as RuleStatusService,
services: alertServices,
logger,
- refresh: false,
buildRuleMessage,
listClient: getListClientMock(),
+ bulkCreate: jest.fn(),
+ wrapHits: jest.fn(),
});
expect(logger.warn).toHaveBeenCalled();
expect(logger.warn.mock.calls[0][0]).toContain('Machine learning job(s) are not started');
- expect(ruleStatusService.partialFailure).toHaveBeenCalled();
- expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain(
- 'Machine learning job(s) are not started'
- );
+ expect(response.warningMessages.length).toEqual(1);
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts
index 28703046289f5..f5c7d8822b51f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts
@@ -17,13 +17,11 @@ import { ListClient } from '../../../../../../lists/server';
import { isJobStarted } from '../../../../../common/machine_learning/helpers';
import { SetupPlugins } from '../../../../plugin';
import { MachineLearningRuleParams } from '../../schemas/rule_schemas';
-import { RefreshTypes } from '../../types';
import { bulkCreateMlSignals } from '../bulk_create_ml_signals';
import { filterEventsAgainstList } from '../filters/filter_events_against_list';
import { findMlSignals } from '../find_ml_signals';
import { BuildRuleMessage } from '../rule_messages';
-import { RuleStatusService } from '../rule_status_service';
-import { AlertAttributes } from '../types';
+import { AlertAttributes, BulkCreate, WrapHits } from '../types';
import { createErrorsFromShard, createSearchAfterReturnType, mergeReturns } from '../utils';
export const mlExecutor = async ({
@@ -31,21 +29,21 @@ export const mlExecutor = async ({
ml,
listClient,
exceptionItems,
- ruleStatusService,
services,
logger,
- refresh,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
}: {
rule: SavedObject>;
ml: SetupPlugins['ml'];
listClient: ListClient;
exceptionItems: ExceptionListItemSchema[];
- ruleStatusService: RuleStatusService;
services: AlertServices;
logger: Logger;
- refresh: RefreshTypes;
buildRuleMessage: BuildRuleMessage;
+ bulkCreate: BulkCreate;
+ wrapHits: WrapHits;
}) => {
const result = createSearchAfterReturnType();
const ruleParams = rule.attributes.params;
@@ -67,7 +65,7 @@ export const mlExecutor = async ({
jobSummaries.length < 1 ||
jobSummaries.some((job) => !isJobStarted(job.jobState, job.datafeedState))
) {
- const errorMessage = buildRuleMessage(
+ const warningMessage = buildRuleMessage(
'Machine learning job(s) are not started:',
...jobSummaries.map((job) =>
[
@@ -77,9 +75,9 @@ export const mlExecutor = async ({
].join(', ')
)
);
- logger.warn(errorMessage);
+ result.warningMessages.push(warningMessage);
+ logger.warn(warningMessage);
result.warning = true;
- await ruleStatusService.partialFailure(errorMessage);
}
const anomalyResults = await findMlSignals({
@@ -120,8 +118,9 @@ export const mlExecutor = async ({
logger,
id: rule.id,
signalsIndex: ruleParams.outputIndex,
- refresh,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
// The legacy ES client does not define failures when it can be present on the structure, hence why I have the & { failures: [] }
const shardFailures =
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts
index 05e2e3056e99e..9d76a06afa275 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts
@@ -14,11 +14,10 @@ import {
AlertServices,
} from '../../../../../../alerting/server';
import { ListClient } from '../../../../../../lists/server';
-import { RefreshTypes } from '../../types';
import { getFilter } from '../get_filter';
import { getInputIndex } from '../get_input_output_index';
import { searchAfterAndBulkCreate } from '../search_after_bulk_create';
-import { AlertAttributes, RuleRangeTuple } from '../types';
+import { AlertAttributes, RuleRangeTuple, BulkCreate, WrapHits } from '../types';
import { TelemetryEventsSender } from '../../../telemetry/sender';
import { BuildRuleMessage } from '../rule_messages';
import { QueryRuleParams, SavedQueryRuleParams } from '../../schemas/rule_schemas';
@@ -32,9 +31,10 @@ export const queryExecutor = async ({
version,
searchAfterSize,
logger,
- refresh,
eventsTelemetry,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
}: {
rule: SavedObject>;
tuples: RuleRangeTuple[];
@@ -44,9 +44,10 @@ export const queryExecutor = async ({
version: string;
searchAfterSize: number;
logger: Logger;
- refresh: RefreshTypes;
eventsTelemetry: TelemetryEventsSender | undefined;
buildRuleMessage: BuildRuleMessage;
+ bulkCreate: BulkCreate;
+ wrapHits: WrapHits;
}) => {
const ruleParams = rule.attributes.params;
const inputIndex = await getInputIndex(services, version, ruleParams.index);
@@ -74,7 +75,8 @@ export const queryExecutor = async ({
signalsIndex: ruleParams.outputIndex,
filter: esFilter,
pageSize: searchAfterSize,
- refresh,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts
index 10b4ce939ca3a..078eb8362069c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts
@@ -14,9 +14,8 @@ import {
AlertServices,
} from '../../../../../../alerting/server';
import { ListClient } from '../../../../../../lists/server';
-import { RefreshTypes } from '../../types';
import { getInputIndex } from '../get_input_output_index';
-import { RuleRangeTuple, AlertAttributes } from '../types';
+import { RuleRangeTuple, AlertAttributes, BulkCreate, WrapHits } from '../types';
import { TelemetryEventsSender } from '../../../telemetry/sender';
import { BuildRuleMessage } from '../rule_messages';
import { createThreatSignals } from '../threat_mapping/create_threat_signals';
@@ -31,9 +30,10 @@ export const threatMatchExecutor = async ({
version,
searchAfterSize,
logger,
- refresh,
eventsTelemetry,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
}: {
rule: SavedObject>;
tuples: RuleRangeTuple[];
@@ -43,9 +43,10 @@ export const threatMatchExecutor = async ({
version: string;
searchAfterSize: number;
logger: Logger;
- refresh: RefreshTypes;
eventsTelemetry: TelemetryEventsSender | undefined;
buildRuleMessage: BuildRuleMessage;
+ bulkCreate: BulkCreate;
+ wrapHits: WrapHits;
}) => {
const ruleParams = rule.attributes.params;
const inputIndex = await getInputIndex(services, version, ruleParams.index);
@@ -67,7 +68,6 @@ export const threatMatchExecutor = async ({
outputIndex: ruleParams.outputIndex,
ruleSO: rule,
searchAfterSize,
- refresh,
threatFilters: ruleParams.threatFilters ?? [],
threatQuery: ruleParams.threatQuery,
threatLanguage: ruleParams.threatLanguage,
@@ -76,5 +76,7 @@ export const threatMatchExecutor = async ({
threatIndicatorPath: ruleParams.threatIndicatorPath,
concurrentSearches: ruleParams.concurrentSearches ?? 1,
itemsPerSearch: ruleParams.itemsPerSearch ?? 9000,
+ bulkCreate,
+ wrapHits,
});
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts
index 5d62b28b73ae8..f03e8b8a147ae 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts
@@ -7,7 +7,6 @@
import { loggingSystemMock } from 'src/core/server/mocks';
import { alertsMock, AlertServicesMock } from '../../../../../../alerting/server/mocks';
-import { RuleStatusService } from '../rule_status_service';
import { thresholdExecutor } from './threshold';
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getEntryListMock } from '../../../../../../lists/common/schemas/types/entry_list.mock';
@@ -18,7 +17,6 @@ describe('threshold_executor', () => {
const version = '8.0.0';
let logger: ReturnType;
let alertServices: AlertServicesMock;
- let ruleStatusService: Record;
const thresholdSO = {
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
type: 'alert',
@@ -50,34 +48,24 @@ describe('threshold_executor', () => {
beforeEach(() => {
alertServices = alertsMock.createAlertServices();
logger = loggingSystemMock.createLogger();
- ruleStatusService = {
- success: jest.fn(),
- find: jest.fn(),
- goingToRun: jest.fn(),
- error: jest.fn(),
- partialFailure: jest.fn(),
- };
});
describe('thresholdExecutor', () => {
it('should set a warning when exception list for threshold rule contains value list exceptions', async () => {
const exceptionItems = [getExceptionListItemSchemaMock({ entries: [getEntryListMock()] })];
- await thresholdExecutor({
+ const response = await thresholdExecutor({
rule: thresholdSO,
tuples: [],
exceptionItems,
- ruleStatusService: (ruleStatusService as unknown) as RuleStatusService,
services: alertServices,
version,
logger,
- refresh: false,
buildRuleMessage,
startedAt: new Date(),
+ bulkCreate: jest.fn(),
+ wrapHits: jest.fn(),
});
- expect(ruleStatusService.partialFailure).toHaveBeenCalled();
- expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain(
- 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules'
- );
+ expect(response.warningMessages.length).toEqual(1);
});
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts
index fa0986044e250..5e23128c9c148 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts
@@ -15,51 +15,55 @@ import {
} from '../../../../../../alerting/server';
import { hasLargeValueItem } from '../../../../../common/detection_engine/utils';
import { ThresholdRuleParams } from '../../schemas/rule_schemas';
-import { RefreshTypes } from '../../types';
import { getFilter } from '../get_filter';
import { getInputIndex } from '../get_input_output_index';
-import { BuildRuleMessage } from '../rule_messages';
-import { RuleStatusService } from '../rule_status_service';
import {
bulkCreateThresholdSignals,
findThresholdSignals,
getThresholdBucketFilters,
getThresholdSignalHistory,
} from '../threshold';
-import { AlertAttributes, RuleRangeTuple, SearchAfterAndBulkCreateReturnType } from '../types';
+import {
+ AlertAttributes,
+ BulkCreate,
+ RuleRangeTuple,
+ SearchAfterAndBulkCreateReturnType,
+ WrapHits,
+} from '../types';
import {
createSearchAfterReturnType,
createSearchAfterReturnTypeFromResponse,
mergeReturns,
} from '../utils';
+import { BuildRuleMessage } from '../rule_messages';
export const thresholdExecutor = async ({
rule,
tuples,
exceptionItems,
- ruleStatusService,
services,
version,
logger,
- refresh,
buildRuleMessage,
startedAt,
+ bulkCreate,
+ wrapHits,
}: {
rule: SavedObject>;
tuples: RuleRangeTuple[];
exceptionItems: ExceptionListItemSchema[];
- ruleStatusService: RuleStatusService;
services: AlertServices;
version: string;
logger: Logger;
- refresh: RefreshTypes;
buildRuleMessage: BuildRuleMessage;
startedAt: Date;
+ bulkCreate: BulkCreate;
+ wrapHits: WrapHits;
}): Promise => {
let result = createSearchAfterReturnType();
const ruleParams = rule.attributes.params;
if (hasLargeValueItem(exceptionItems)) {
- await ruleStatusService.partialFailure(
+ result.warningMessages.push(
'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules'
);
result.warning = true;
@@ -126,14 +130,13 @@ export const thresholdExecutor = async ({
filter: esFilter,
services,
logger,
- id: rule.id,
inputIndexPattern: inputIndex,
signalsIndex: ruleParams.outputIndex,
startedAt,
from: tuple.from.toDate(),
- refresh,
thresholdSignalHistory,
- buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
result = mergeReturns([
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.test.ts
new file mode 100644
index 0000000000000..5c4af83c3b03e
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.test.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { filterDuplicateSignals } from './filter_duplicate_signals';
+import { sampleWrappedSignalHit } from './__mocks__/es_results';
+
+const mockRuleId1 = 'aaaaaaaa';
+const mockRuleId2 = 'bbbbbbbb';
+const mockRuleId3 = 'cccccccc';
+
+const createWrappedSignalHitWithRuleId = (ruleId: string) => {
+ const mockSignal = sampleWrappedSignalHit();
+ return {
+ ...mockSignal,
+ _source: {
+ ...mockSignal._source,
+ signal: {
+ ...mockSignal._source.signal,
+ ancestors: [
+ {
+ ...mockSignal._source.signal.ancestors[0],
+ rule: ruleId,
+ },
+ ],
+ },
+ },
+ };
+};
+const mockSignals = [
+ createWrappedSignalHitWithRuleId(mockRuleId1),
+ createWrappedSignalHitWithRuleId(mockRuleId2),
+];
+
+describe('filterDuplicateSignals', () => {
+ it('filters duplicate signals', () => {
+ expect(filterDuplicateSignals(mockRuleId1, mockSignals).length).toEqual(1);
+ });
+
+ it('does not filter non-duplicate signals', () => {
+ expect(filterDuplicateSignals(mockRuleId3, mockSignals).length).toEqual(2);
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts
new file mode 100644
index 0000000000000..a648c05306289
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { WrappedSignalHit } from './types';
+
+export const filterDuplicateSignals = (ruleId: string, signals: WrappedSignalHit[]) => {
+ return signals.filter(
+ (doc) => !doc._source.signal?.ancestors.some((ancestor) => ancestor.rule === ruleId)
+ );
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
index 52c887c3ca55a..e4eb7e854f670 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
@@ -16,29 +16,28 @@ import {
sampleDocWithSortId,
} from './__mocks__/es_results';
import { searchAfterAndBulkCreate } from './search_after_bulk_create';
-import { buildRuleMessageFactory } from './rule_messages';
import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants';
import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks';
import uuid from 'uuid';
import { listMock } from '../../../../../lists/server/mocks';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
-import { BulkResponse, RuleRangeTuple } from './types';
+import { BulkCreate, BulkResponse, RuleRangeTuple, WrapHits } from './types';
import type { SearchListItemArraySchema } from '@kbn/securitysolution-io-ts-list-types';
import { getSearchListItemResponseMock } from '../../../../../lists/common/schemas/response/search_list_item_schema.mock';
import { getRuleRangeTuples } from './utils';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
+import { bulkCreateFactory } from './bulk_create_factory';
+import { wrapHitsFactory } from './wrap_hits_factory';
+import { mockBuildRuleMessage } from './__mocks__/build_rule_message.mock';
-const buildRuleMessage = buildRuleMessageFactory({
- id: 'fake id',
- ruleId: 'fake rule id',
- index: 'fakeindex',
- name: 'fake name',
-});
+const buildRuleMessage = mockBuildRuleMessage;
describe('searchAfterAndBulkCreate', () => {
let mockService: AlertServicesMock;
+ let bulkCreate: BulkCreate;
+ let wrapHits: WrapHits;
let inputIndexPattern: string[] = [];
let listClient = listMock.getListClient();
const someGuids = Array.from({ length: 13 }).map(() => uuid.v4());
@@ -61,6 +60,13 @@ describe('searchAfterAndBulkCreate', () => {
maxSignals: sampleParams.maxSignals,
buildRuleMessage,
}));
+ bulkCreate = bulkCreateFactory(
+ mockLogger,
+ mockService.scopedClusterClient.asCurrentUser,
+ buildRuleMessage,
+ false
+ );
+ wrapHits = wrapHitsFactory({ ruleSO, signalsIndex: DEFAULT_SIGNALS_INDEX });
});
test('should return success with number of searches less than max signals', async () => {
@@ -166,6 +172,7 @@ describe('searchAfterAndBulkCreate', () => {
},
},
];
+
const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({
tuples,
ruleSO,
@@ -179,8 +186,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(success).toEqual(true);
expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(5);
@@ -282,8 +290,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(success).toEqual(true);
expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(4);
@@ -359,8 +368,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(success).toEqual(true);
expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(2);
@@ -417,8 +427,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(success).toEqual(true);
expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(2);
@@ -495,8 +506,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(success).toEqual(true);
expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(2);
@@ -549,8 +561,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(success).toEqual(true);
expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
@@ -625,18 +638,14 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(success).toEqual(true);
expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(createdSignalsCount).toEqual(4);
expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000'));
- // I don't like testing log statements since logs change but this is the best
- // way I can think of to ensure this section is getting hit with this test case.
- expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[14][0]).toContain(
- 'ran out of sort ids to sort on name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"'
- );
});
test('should return success when no exceptions list provided', async () => {
@@ -703,8 +712,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(success).toEqual(true);
expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(2);
@@ -746,8 +756,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(mockLogger.error).toHaveBeenCalled();
expect(success).toEqual(false);
@@ -792,8 +803,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(success).toEqual(true);
expect(createdSignalsCount).toEqual(0);
@@ -852,8 +864,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(success).toEqual(false);
expect(createdSignalsCount).toEqual(0); // should not create signals if search threw error
@@ -977,8 +990,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(success).toEqual(false);
expect(errors).toEqual(['error on creation']);
@@ -1072,8 +1086,9 @@ describe('searchAfterAndBulkCreate', () => {
signalsIndex: DEFAULT_SIGNALS_INDEX,
pageSize: 1,
filter: undefined,
- refresh: false,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
expect(mockEnrichment).toHaveBeenCalledWith(
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts
index b0dcc1810a639..bb2e57b0606e5 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts
@@ -8,7 +8,6 @@
import { identity } from 'lodash';
import type { estypes } from '@elastic/elasticsearch';
import { singleSearchAfter } from './single_search_after';
-import { singleBulkCreate } from './single_bulk_create';
import { filterEventsAgainstList } from './filters/filter_events_against_list';
import { sendAlertTelemetryEvents } from './send_telemetry_events';
import {
@@ -31,14 +30,13 @@ export const searchAfterAndBulkCreate = async ({
listClient,
logger,
eventsTelemetry,
- id,
inputIndexPattern,
- signalsIndex,
filter,
pageSize,
- refresh,
buildRuleMessage,
enrichment = identity,
+ bulkCreate,
+ wrapHits,
}: SearchAfterAndBulkCreateParams): Promise => {
const ruleParams = ruleSO.attributes.params;
let toReturn = createSearchAfterReturnType();
@@ -149,6 +147,7 @@ export const searchAfterAndBulkCreate = async ({
);
}
const enrichedEvents = await enrichment(filteredEvents);
+ const wrappedDocs = wrapHits(enrichedEvents.hits.hits);
const {
bulkCreateDuration: bulkDuration,
@@ -156,16 +155,7 @@ export const searchAfterAndBulkCreate = async ({
createdItems,
success: bulkSuccess,
errors: bulkErrors,
- } = await singleBulkCreate({
- buildRuleMessage,
- filteredEvents: enrichedEvents,
- ruleSO,
- services,
- logger,
- id,
- signalsIndex,
- refresh,
- });
+ } = await bulkCreate(wrappedDocs);
toReturn = mergeReturns([
toReturn,
createSearchAfterReturnType({
@@ -180,10 +170,10 @@ export const searchAfterAndBulkCreate = async ({
logger.debug(buildRuleMessage(`created ${createdCount} signals`));
logger.debug(buildRuleMessage(`signalsCreatedCount: ${signalsCreatedCount}`));
logger.debug(
- buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`)
+ buildRuleMessage(`enrichedEvents.hits.hits: ${enrichedEvents.hits.hits.length}`)
);
- sendAlertTelemetryEvents(logger, eventsTelemetry, filteredEvents, buildRuleMessage);
+ sendAlertTelemetryEvents(logger, eventsTelemetry, enrichedEvents, buildRuleMessage);
}
if (!hasSortId) {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
index 823d694f36514..d8c919b50e9db 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
@@ -274,35 +274,6 @@ describe('signal_rule_alert_type', () => {
expect(ruleStatusService.error).toHaveBeenCalledTimes(0);
});
- it("should set refresh to 'wait_for' when actions are present", async () => {
- const ruleAlert = getAlertMock(getQueryRuleParams());
- ruleAlert.actions = [
- {
- actionTypeId: '.slack',
- params: {
- message:
- 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}',
- },
- group: 'default',
- id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
- },
- ];
-
- alertServices.savedObjectsClient.get.mockResolvedValue({
- id: 'id',
- type: 'type',
- references: [],
- attributes: ruleAlert,
- });
- await alert.executor(payload);
- expect((queryExecutor as jest.Mock).mock.calls[0][0].refresh).toEqual('wait_for');
- });
-
- it('should set refresh to false when actions are not present', async () => {
- await alert.executor(payload);
- expect((queryExecutor as jest.Mock).mock.calls[0][0].refresh).toEqual(false);
- });
-
it('should call scheduleActions if signalsCount was greater than 0 and rule has actions defined', async () => {
const ruleAlert = getAlertMock(getQueryRuleParams());
ruleAlert.actions = [
@@ -462,6 +433,7 @@ describe('signal_rule_alert_type', () => {
lastLookBackDate: null,
createdSignalsCount: 0,
createdSignals: [],
+ warningMessages: [],
errors: ['Error that bubbled up.'],
};
(queryExecutor as jest.Mock).mockResolvedValue(result);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index 13a63df6ed8b6..0a2e22bc44b60 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -65,6 +65,8 @@ import {
RuleParams,
savedQueryRuleParams,
} from '../schemas/rule_schemas';
+import { bulkCreateFactory } from './bulk_create_factory';
+import { wrapHitsFactory } from './wrap_hits_factory';
export const signalRulesAlertType = ({
logger,
@@ -218,6 +220,19 @@ export const signalRulesAlertType = ({
client: exceptionsClient,
lists: params.exceptionsList ?? [],
});
+
+ const bulkCreate = bulkCreateFactory(
+ logger,
+ services.scopedClusterClient.asCurrentUser,
+ buildRuleMessage,
+ refresh
+ );
+
+ const wrapHits = wrapHitsFactory({
+ ruleSO: savedObject,
+ signalsIndex: params.outputIndex,
+ });
+
if (isMlRule(type)) {
const mlRuleSO = asTypeSpecificSO(savedObject, machineLearningRuleParams);
result = await mlExecutor({
@@ -225,11 +240,11 @@ export const signalRulesAlertType = ({
ml,
listClient,
exceptionItems,
- ruleStatusService,
services,
logger,
- refresh,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
} else if (isThresholdRule(type)) {
const thresholdRuleSO = asTypeSpecificSO(savedObject, thresholdRuleParams);
@@ -237,13 +252,13 @@ export const signalRulesAlertType = ({
rule: thresholdRuleSO,
tuples,
exceptionItems,
- ruleStatusService,
services,
version,
logger,
- refresh,
buildRuleMessage,
startedAt,
+ bulkCreate,
+ wrapHits,
});
} else if (isThreatMatchRule(type)) {
const threatRuleSO = asTypeSpecificSO(savedObject, threatRuleParams);
@@ -256,9 +271,10 @@ export const signalRulesAlertType = ({
version,
searchAfterSize,
logger,
- refresh,
eventsTelemetry,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
} else if (isQueryRule(type)) {
const queryRuleSO = validateQueryRuleTypes(savedObject);
@@ -271,25 +287,30 @@ export const signalRulesAlertType = ({
version,
searchAfterSize,
logger,
- refresh,
eventsTelemetry,
buildRuleMessage,
+ bulkCreate,
+ wrapHits,
});
} else if (isEqlRule(type)) {
const eqlRuleSO = asTypeSpecificSO(savedObject, eqlRuleParams);
result = await eqlExecutor({
rule: eqlRuleSO,
exceptionItems,
- ruleStatusService,
services,
version,
searchAfterSize,
+ bulkCreate,
logger,
- refresh,
});
} else {
throw new Error(`unknown rule type ${type}`);
}
+ if (result.warningMessages.length) {
+ const warningMessage = buildRuleMessage(result.warningMessages.join());
+ await ruleStatusService.partialFailure(warningMessage);
+ }
+
if (result.success) {
if (actions.length) {
const notificationRuleParams: NotificationRuleTypeParams = {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts
deleted file mode 100644
index 3fbb8c1a607e9..0000000000000
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { generateId } from './utils';
-import {
- sampleDocSearchResultsNoSortId,
- mockLogger,
- sampleRuleGuid,
- sampleDocSearchResultsNoSortIdNoVersion,
- sampleEmptyDocSearchResults,
- sampleBulkCreateDuplicateResult,
- sampleBulkCreateErrorResult,
- sampleDocWithAncestors,
- sampleRuleSO,
-} from './__mocks__/es_results';
-import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants';
-import { singleBulkCreate, filterDuplicateRules } from './single_bulk_create';
-import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks';
-import { buildRuleMessageFactory } from './rule_messages';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
-import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
-
-const buildRuleMessage = buildRuleMessageFactory({
- id: 'fake id',
- ruleId: 'fake rule id',
- index: 'fakeindex',
- name: 'fake name',
-});
-describe('singleBulkCreate', () => {
- const mockService: AlertServicesMock = alertsMock.createAlertServices();
-
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- describe('create signal id gereateId', () => {
- test('two docs with same index, id, and version should have same id', () => {
- const findex = 'myfakeindex';
- const fid = 'somefakeid';
- const version = '1';
- const ruleId = 'rule-1';
- // 'myfakeindexsomefakeid1rule-1'
- const generatedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679';
- const firstHash = generateId(findex, fid, version, ruleId);
- const secondHash = generateId(findex, fid, version, ruleId);
- expect(firstHash).toEqual(generatedHash);
- expect(secondHash).toEqual(generatedHash);
- expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field
- expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512);
- });
- test('two docs with different index, id, and version should have different id', () => {
- const findex = 'myfakeindex';
- const findex2 = 'mysecondfakeindex';
- const fid = 'somefakeid';
- const version = '1';
- const ruleId = 'rule-1';
- // 'myfakeindexsomefakeid1rule-1'
- const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679';
- // 'mysecondfakeindexsomefakeid1rule-1'
- const secondGeneratedHash =
- 'a852941273f805ffe9006e574601acc8ae1148d6c0b3f7f8c4785cba8f6b768a';
- const firstHash = generateId(findex, fid, version, ruleId);
- const secondHash = generateId(findex2, fid, version, ruleId);
- expect(firstHash).toEqual(firstGeneratedHash);
- expect(secondHash).toEqual(secondGeneratedHash);
- expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field
- expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512);
- expect(firstHash).not.toEqual(secondHash);
- });
- test('two docs with same index, different id, and same version should have different id', () => {
- const findex = 'myfakeindex';
- const fid = 'somefakeid';
- const fid2 = 'somefakeid2';
- const version = '1';
- const ruleId = 'rule-1';
- // 'myfakeindexsomefakeid1rule-1'
- const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679';
- // 'myfakeindexsomefakeid21rule-1'
- const secondGeneratedHash =
- '7d33faea18159fd010c4b79890620e8b12cdc88ec1d370149d0e5552ce860255';
- const firstHash = generateId(findex, fid, version, ruleId);
- const secondHash = generateId(findex, fid2, version, ruleId);
- expect(firstHash).toEqual(firstGeneratedHash);
- expect(secondHash).toEqual(secondGeneratedHash);
- expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field
- expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512);
- expect(firstHash).not.toEqual(secondHash);
- });
- test('two docs with same index, same id, and different version should have different id', () => {
- const findex = 'myfakeindex';
- const fid = 'somefakeid';
- const version = '1';
- const version2 = '2';
- const ruleId = 'rule-1';
- // 'myfakeindexsomefakeid1rule-1'
- const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679';
- // myfakeindexsomefakeid2rule-1'
- const secondGeneratedHash =
- 'f016f3071fa9df9221d2fb2ba92389d4d388a4347c6ec7a4012c01cb1c640a40';
- const firstHash = generateId(findex, fid, version, ruleId);
- const secondHash = generateId(findex, fid, version2, ruleId);
- expect(firstHash).toEqual(firstGeneratedHash);
- expect(secondHash).toEqual(secondGeneratedHash);
- expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field
- expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512);
- expect(firstHash).not.toEqual(secondHash);
- });
- test('Ensure generated id is less than 512 bytes, even for really really long strings', () => {
- const longIndexName =
- 'myfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindex';
- const fid = 'somefakeid';
- const version = '1';
- const ruleId = 'rule-1';
- const firstHash = generateId(longIndexName, fid, version, ruleId);
- expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field
- });
- test('two docs with same index, same id, same version number, and different rule ids should have different id', () => {
- const findex = 'myfakeindex';
- const fid = 'somefakeid';
- const version = '1';
- const ruleId = 'rule-1';
- const ruleId2 = 'rule-2';
- // 'myfakeindexsomefakeid1rule-1'
- const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679';
- // myfakeindexsomefakeid1rule-2'
- const secondGeneratedHash =
- '1eb04f997086f8b3b143d4d9b18ac178c4a7423f71a5dad9ba8b9e92603c6863';
- const firstHash = generateId(findex, fid, version, ruleId);
- const secondHash = generateId(findex, fid, version, ruleId2);
- expect(firstHash).toEqual(firstGeneratedHash);
- expect(secondHash).toEqual(secondGeneratedHash);
- expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field
- expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512);
- expect(firstHash).not.toEqual(secondHash);
- });
- });
-
- test('create successful bulk create', async () => {
- const ruleSO = sampleRuleSO(getQueryRuleParams());
- mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce(
- // @ts-expect-error not compatible response interface
- elasticsearchClientMock.createSuccessTransportRequestPromise({
- took: 100,
- errors: false,
- items: [
- {
- fakeItemValue: 'fakeItemKey',
- },
- ],
- })
- );
- const { success, createdItemsCount } = await singleBulkCreate({
- filteredEvents: sampleDocSearchResultsNoSortId(),
- ruleSO,
- services: mockService,
- logger: mockLogger,
- id: sampleRuleGuid,
- signalsIndex: DEFAULT_SIGNALS_INDEX,
- refresh: false,
- buildRuleMessage,
- });
- expect(success).toEqual(true);
- expect(createdItemsCount).toEqual(0);
- });
-
- test('create successful bulk create with docs with no versioning', async () => {
- const ruleSO = sampleRuleSO(getQueryRuleParams());
- mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce(
- // @ts-expect-error not compatible response interface
- elasticsearchClientMock.createSuccessTransportRequestPromise({
- took: 100,
- errors: false,
- items: [
- {
- fakeItemValue: 'fakeItemKey',
- },
- ],
- })
- );
- const { success, createdItemsCount } = await singleBulkCreate({
- filteredEvents: sampleDocSearchResultsNoSortIdNoVersion(),
- ruleSO,
- services: mockService,
- logger: mockLogger,
- id: sampleRuleGuid,
- signalsIndex: DEFAULT_SIGNALS_INDEX,
- refresh: false,
- buildRuleMessage,
- });
- expect(success).toEqual(true);
- expect(createdItemsCount).toEqual(0);
- });
-
- test('create unsuccessful bulk create due to empty search results', async () => {
- const ruleSO = sampleRuleSO(getQueryRuleParams());
- mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValue(
- // @ts-expect-error not full response interface
- elasticsearchClientMock.createSuccessTransportRequestPromise(false)
- );
- const { success, createdItemsCount } = await singleBulkCreate({
- filteredEvents: sampleEmptyDocSearchResults(),
- ruleSO,
- services: mockService,
- logger: mockLogger,
- id: sampleRuleGuid,
- signalsIndex: DEFAULT_SIGNALS_INDEX,
- refresh: false,
- buildRuleMessage,
- });
- expect(success).toEqual(true);
- expect(createdItemsCount).toEqual(0);
- });
-
- test('create successful bulk create when bulk create has duplicate errors', async () => {
- const ruleSO = sampleRuleSO(getQueryRuleParams());
- mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValue(
- elasticsearchClientMock.createSuccessTransportRequestPromise(sampleBulkCreateDuplicateResult)
- );
- const { success, createdItemsCount } = await singleBulkCreate({
- filteredEvents: sampleDocSearchResultsNoSortId(),
- ruleSO,
- services: mockService,
- logger: mockLogger,
- id: sampleRuleGuid,
- signalsIndex: DEFAULT_SIGNALS_INDEX,
- refresh: false,
- buildRuleMessage,
- });
-
- expect(mockLogger.error).not.toHaveBeenCalled();
- expect(success).toEqual(true);
- expect(createdItemsCount).toEqual(1);
- });
-
- test('create failed bulk create when bulk create has multiple error statuses', async () => {
- const ruleSO = sampleRuleSO(getQueryRuleParams());
- mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValue(
- elasticsearchClientMock.createSuccessTransportRequestPromise(sampleBulkCreateErrorResult)
- );
- const { success, createdItemsCount, errors } = await singleBulkCreate({
- filteredEvents: sampleDocSearchResultsNoSortId(),
- ruleSO,
- services: mockService,
- logger: mockLogger,
- id: sampleRuleGuid,
- signalsIndex: DEFAULT_SIGNALS_INDEX,
- refresh: false,
- buildRuleMessage,
- });
- expect(mockLogger.error).toHaveBeenCalled();
- expect(errors).toEqual(['[4]: internal server error']);
- expect(success).toEqual(false);
- expect(createdItemsCount).toEqual(1);
- });
-
- test('filter duplicate rules will return an empty array given an empty array', () => {
- const filtered = filterDuplicateRules(
- '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
- sampleEmptyDocSearchResults()
- );
- expect(filtered).toEqual([]);
- });
-
- test('filter duplicate rules will return nothing filtered when the two rule ids do not match with each other', () => {
- const filtered = filterDuplicateRules('some id', sampleDocWithAncestors());
- expect(filtered).toEqual(sampleDocWithAncestors().hits.hits);
- });
-
- test('filters duplicate rules will return empty array when the two rule ids match each other', () => {
- const filtered = filterDuplicateRules(
- '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
- sampleDocWithAncestors()
- );
- expect(filtered).toEqual([]);
- });
-
- test('filter duplicate rules will return back search responses if they do not have a signal and will NOT filter the source out', () => {
- const ancestors = sampleDocSearchResultsNoSortId();
- const filtered = filterDuplicateRules('04128c15-0d1b-4716-a4c5-46997ac7f3bd', ancestors);
- expect(filtered).toEqual(ancestors.hits.hits);
- });
-
- test('filter duplicate rules does not attempt filters when the signal is not an event type of signal but rather a "clash" from the source index having its own numeric signal type', () => {
- const doc = { ...sampleDocWithAncestors(), _source: { signal: 1234 } };
- const filtered = filterDuplicateRules('04128c15-0d1b-4716-a4c5-46997ac7f3bd', doc);
- expect(filtered).toEqual([]);
- });
-
- test('filter duplicate rules does not attempt filters when the signal is not an event type of signal but rather a "clash" from the source index having its own object signal type', () => {
- const doc = { ...sampleDocWithAncestors(), _source: { signal: {} } };
- const filtered = filterDuplicateRules('04128c15-0d1b-4716-a4c5-46997ac7f3bd', doc);
- expect(filtered).toEqual([]);
- });
-
- test('create successful and returns proper createdItemsCount', async () => {
- const ruleSO = sampleRuleSO(getQueryRuleParams());
- mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce(
- elasticsearchClientMock.createSuccessTransportRequestPromise(sampleBulkCreateDuplicateResult)
- );
- const { success, createdItemsCount } = await singleBulkCreate({
- filteredEvents: sampleDocSearchResultsNoSortId(),
- ruleSO,
- services: mockService,
- logger: mockLogger,
- id: sampleRuleGuid,
- signalsIndex: DEFAULT_SIGNALS_INDEX,
- refresh: false,
- buildRuleMessage,
- });
- expect(success).toEqual(true);
- expect(createdItemsCount).toEqual(1);
- });
-});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts
deleted file mode 100644
index 92d01fef6e50c..0000000000000
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { countBy, isEmpty, get } from 'lodash';
-import { performance } from 'perf_hooks';
-import {
- AlertInstanceContext,
- AlertInstanceState,
- AlertServices,
-} from '../../../../../alerting/server';
-import { AlertAttributes, SignalHit, SignalSearchResponse, WrappedSignalHit } from './types';
-import { RefreshTypes } from '../types';
-import { generateId, makeFloatString, errorAggregator } from './utils';
-import { buildBulkBody } from './build_bulk_body';
-import { BuildRuleMessage } from './rule_messages';
-import { Logger, SavedObject } from '../../../../../../../src/core/server';
-import { isEventTypeSignal } from './build_event_type_signal';
-
-interface SingleBulkCreateParams {
- filteredEvents: SignalSearchResponse;
- ruleSO: SavedObject;
- services: AlertServices;
- logger: Logger;
- id: string;
- signalsIndex: string;
- refresh: RefreshTypes;
- buildRuleMessage: BuildRuleMessage;
-}
-
-/**
- * This is for signals on signals to work correctly. If given a rule id this will check if
- * that rule id already exists in the ancestor tree of each signal search response and remove
- * those documents so they cannot be created as a signal since we do not want a rule id to
- * ever be capable of re-writing the same signal continuously if both the _input_ and _output_
- * of the signals index happens to be the same index.
- * @param ruleId The rule id
- * @param signalSearchResponse The search response that has all the documents
- */
-export const filterDuplicateRules = (
- ruleId: string,
- signalSearchResponse: SignalSearchResponse
-) => {
- return signalSearchResponse.hits.hits.filter((doc) => {
- if (doc._source?.signal == null || !isEventTypeSignal(doc)) {
- return true;
- } else {
- return !(
- doc._source?.signal.ancestors.some((ancestor) => ancestor.rule === ruleId) ||
- doc._source?.signal.rule.id === ruleId
- );
- }
- });
-};
-
-/**
- * Similar to filterDuplicateRules, but operates on candidate signal documents rather than events that matched
- * the detection query. This means we only have to compare the ruleId against the ancestors array.
- * @param ruleId The rule id
- * @param signals The candidate new signals
- */
-export const filterDuplicateSignals = (ruleId: string, signals: WrappedSignalHit[]) => {
- return signals.filter(
- (doc) => !doc._source.signal?.ancestors.some((ancestor) => ancestor.rule === ruleId)
- );
-};
-
-export interface SingleBulkCreateResponse {
- success: boolean;
- bulkCreateDuration?: string;
- createdItemsCount: number;
- createdItems: SignalHit[];
- errors: string[];
-}
-
-export interface BulkInsertSignalsResponse {
- bulkCreateDuration: string;
- createdItemsCount: number;
- createdItems: SignalHit[];
-}
-
-// Bulk Index documents.
-export const singleBulkCreate = async ({
- buildRuleMessage,
- filteredEvents,
- ruleSO,
- services,
- logger,
- id,
- signalsIndex,
- refresh,
-}: SingleBulkCreateParams): Promise => {
- const ruleParams = ruleSO.attributes.params;
- filteredEvents.hits.hits = filterDuplicateRules(id, filteredEvents);
- logger.debug(buildRuleMessage(`about to bulk create ${filteredEvents.hits.hits.length} events`));
- if (filteredEvents.hits.hits.length === 0) {
- logger.debug(buildRuleMessage(`all events were duplicates`));
- return { success: true, createdItemsCount: 0, createdItems: [], errors: [] };
- }
- // index documents after creating an ID based on the
- // source documents' originating index, and the original
- // document _id. This will allow two documents from two
- // different indexes with the same ID to be
- // indexed, and prevents us from creating any updates
- // to the documents once inserted into the signals index,
- // while preventing duplicates from being added to the
- // signals index if rules are re-run over the same time
- // span. Also allow for versioning.
- const bulkBody = filteredEvents.hits.hits.flatMap((doc) => [
- {
- create: {
- _index: signalsIndex,
- _id: generateId(
- doc._index,
- doc._id,
- doc._version ? doc._version.toString() : '',
- ruleParams.ruleId ?? ''
- ),
- },
- },
- buildBulkBody(ruleSO, doc),
- ]);
- const start = performance.now();
- const { body: response } = await services.scopedClusterClient.asCurrentUser.bulk({
- index: signalsIndex,
- refresh,
- body: bulkBody,
- });
- const end = performance.now();
- logger.debug(
- buildRuleMessage(
- `individual bulk process time took: ${makeFloatString(end - start)} milliseconds`
- )
- );
- logger.debug(buildRuleMessage(`took property says bulk took: ${response.took} milliseconds`));
- const createdItems = filteredEvents.hits.hits
- .map((doc, index) => ({
- _id: response.items[index].create?._id ?? '',
- _index: response.items[index].create?._index ?? '',
- ...buildBulkBody(ruleSO, doc),
- }))
- .filter((_, index) => get(response.items[index], 'create.status') === 201);
- const createdItemsCount = createdItems.length;
- const duplicateSignalsCount = countBy(response.items, 'create.status')['409'];
- const errorCountByMessage = errorAggregator(response, [409]);
-
- logger.debug(buildRuleMessage(`bulk created ${createdItemsCount} signals`));
- if (duplicateSignalsCount > 0) {
- logger.debug(buildRuleMessage(`ignored ${duplicateSignalsCount} duplicate signals`));
- }
-
- if (!isEmpty(errorCountByMessage)) {
- logger.error(
- buildRuleMessage(
- `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}`
- )
- );
- return {
- errors: Object.keys(errorCountByMessage),
- success: false,
- bulkCreateDuration: makeFloatString(end - start),
- createdItemsCount,
- createdItems,
- };
- } else {
- return {
- errors: [],
- success: true,
- bulkCreateDuration: makeFloatString(end - start),
- createdItemsCount,
- createdItems,
- };
- }
-};
-
-// Bulk Index new signals.
-export const bulkInsertSignals = async (
- signals: WrappedSignalHit[],
- logger: Logger,
- services: AlertServices,
- refresh: RefreshTypes
-): Promise => {
- // index documents after creating an ID based on the
- // id and index of each parent and the rule ID
- const bulkBody = signals.flatMap((doc) => [
- {
- create: {
- _index: doc._index,
- _id: doc._id,
- },
- },
- doc._source,
- ]);
- const start = performance.now();
- const { body: response } = await services.scopedClusterClient.asCurrentUser.bulk({
- refresh,
- body: bulkBody,
- });
- const end = performance.now();
- logger.debug(`individual bulk process time took: ${makeFloatString(end - start)} milliseconds`);
- logger.debug(`took property says bulk took: ${response.took} milliseconds`);
-
- if (response.errors) {
- const duplicateSignalsCount = countBy(response.items, 'create.status')['409'];
- logger.debug(`ignored ${duplicateSignalsCount} duplicate signals`);
- const errorCountByMessage = errorAggregator(response, [409]);
- if (!isEmpty(errorCountByMessage)) {
- logger.error(
- `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}`
- );
- }
- }
-
- const createdItemsCount = countBy(response.items, 'create.status')['201'] ?? 0;
- const createdItems = signals
- .map((doc, index) => ({
- ...doc._source,
- _id: response.items[index].create?._id ?? '',
- _index: response.items[index].create?._index ?? '',
- }))
- .filter((_, index) => get(response.items[index], 'create.status') === 201);
- logger.debug(`bulk created ${createdItemsCount} signals`);
- return { bulkCreateDuration: makeFloatString(end - start), createdItems, createdItemsCount };
-};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts
index 37b0b88d88eda..3e30a08f1ae69 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts
@@ -31,10 +31,11 @@ export const createThreatSignal = async ({
outputIndex,
ruleSO,
searchAfterSize,
- refresh,
buildRuleMessage,
currentThreatList,
currentResult,
+ bulkCreate,
+ wrapHits,
}: CreateThreatSignalOptions): Promise => {
const threatFilter = buildThreatMappingFilter({
threatMapping,
@@ -81,9 +82,10 @@ export const createThreatSignal = async ({
signalsIndex: outputIndex,
filter: esFilter,
pageSize: searchAfterSize,
- refresh,
buildRuleMessage,
enrichment: threatEnrichment,
+ bulkCreate,
+ wrapHits,
});
logger.debug(
buildRuleMessage(
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
index b3e0e376c7794..5054ab1b2cca5 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
@@ -32,7 +32,6 @@ export const createThreatSignals = async ({
outputIndex,
ruleSO,
searchAfterSize,
- refresh,
threatFilters,
threatQuery,
threatLanguage,
@@ -41,6 +40,8 @@ export const createThreatSignals = async ({
threatIndicatorPath,
concurrentSearches,
itemsPerSearch,
+ bulkCreate,
+ wrapHits,
}: CreateThreatSignalsOptions): Promise => {
const params = ruleSO.attributes.params;
logger.debug(buildRuleMessage('Indicator matching rule starting'));
@@ -55,6 +56,7 @@ export const createThreatSignals = async ({
createdSignalsCount: 0,
createdSignals: [],
errors: [],
+ warningMessages: [],
};
let threatListCount = await getThreatListCount({
@@ -120,10 +122,11 @@ export const createThreatSignals = async ({
outputIndex,
ruleSO,
searchAfterSize,
- refresh,
buildRuleMessage,
currentThreatList: slicedChunk,
currentResult: results,
+ bulkCreate,
+ wrapHits,
})
);
const searchesPerformed = await Promise.all(concurrentSearchesPerformed);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts
index acb64f826f3f2..34b064b0f8805 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts
@@ -29,9 +29,11 @@ import { TelemetryEventsSender } from '../../../telemetry/sender';
import { BuildRuleMessage } from '../rule_messages';
import {
AlertAttributes,
+ BulkCreate,
RuleRangeTuple,
SearchAfterAndBulkCreateReturnType,
SignalsEnrichment,
+ WrapHits,
} from '../types';
import { ThreatRuleParams } from '../../schemas/rule_schemas';
@@ -55,7 +57,6 @@ export interface CreateThreatSignalsOptions {
outputIndex: string;
ruleSO: SavedObject>;
searchAfterSize: number;
- refresh: false | 'wait_for';
threatFilters: unknown[];
threatQuery: ThreatQuery;
buildRuleMessage: BuildRuleMessage;
@@ -64,6 +65,8 @@ export interface CreateThreatSignalsOptions {
threatLanguage: ThreatLanguageOrUndefined;
concurrentSearches: ConcurrentSearches;
itemsPerSearch: ItemsPerSearch;
+ bulkCreate: BulkCreate;
+ wrapHits: WrapHits;
}
export interface CreateThreatSignalOptions {
@@ -85,10 +88,11 @@ export interface CreateThreatSignalOptions {
outputIndex: string;
ruleSO: SavedObject>;
searchAfterSize: number;
- refresh: false | 'wait_for';
buildRuleMessage: BuildRuleMessage;
currentThreatList: ThreatListItem[];
currentResult: SearchAfterAndBulkCreateReturnType;
+ bulkCreate: BulkCreate;
+ wrapHits: WrapHits;
}
export interface BuildThreatMappingFilterOptions {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
index 6c6447bad0975..ec826b44023f6 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
@@ -58,6 +58,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult: SearchAfterAndBulkCreateReturnType = {
@@ -69,6 +70,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineResults(existingResult, newResult);
expect(combinedResults.success).toEqual(true);
@@ -84,6 +86,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult: SearchAfterAndBulkCreateReturnType = {
@@ -95,6 +98,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineResults(existingResult, newResult);
expect(combinedResults.success).toEqual(false);
@@ -110,6 +114,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult: SearchAfterAndBulkCreateReturnType = {
@@ -121,6 +126,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineResults(existingResult, newResult);
expect(combinedResults.lastLookBackDate?.toISOString()).toEqual('2020-09-16T03:34:32.390Z');
@@ -136,6 +142,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult: SearchAfterAndBulkCreateReturnType = {
@@ -147,6 +154,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineResults(existingResult, newResult);
expect(combinedResults).toEqual(
@@ -167,6 +175,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: ['error 1', 'error 2', 'error 3'],
+ warningMessages: [],
};
const newResult: SearchAfterAndBulkCreateReturnType = {
@@ -178,6 +187,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: ['error 4', 'error 1', 'error 3', 'error 5'],
+ warningMessages: [],
};
const combinedResults = combineResults(existingResult, newResult);
expect(combinedResults).toEqual(
@@ -289,6 +299,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const expectedResult: SearchAfterAndBulkCreateReturnType = {
success: true,
@@ -299,6 +310,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineConcurrentResults(existingResult, []);
expect(combinedResults).toEqual(expectedResult);
@@ -314,6 +326,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult: SearchAfterAndBulkCreateReturnType = {
success: true,
@@ -324,6 +337,7 @@ describe('utils', () => {
createdSignalsCount: 0,
createdSignals: [],
errors: [],
+ warningMessages: [],
};
const expectedResult: SearchAfterAndBulkCreateReturnType = {
success: true,
@@ -334,6 +348,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineConcurrentResults(existingResult, [newResult]);
@@ -350,6 +365,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult1: SearchAfterAndBulkCreateReturnType = {
success: true,
@@ -360,6 +376,7 @@ describe('utils', () => {
createdSignalsCount: 5,
createdSignals: Array(5).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult2: SearchAfterAndBulkCreateReturnType = {
success: true,
@@ -370,6 +387,7 @@ describe('utils', () => {
createdSignalsCount: 8,
createdSignals: Array(8).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const expectedResult: SearchAfterAndBulkCreateReturnType = {
@@ -381,6 +399,7 @@ describe('utils', () => {
createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3)
createdSignals: Array(16).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineConcurrentResults(existingResult, [newResult1, newResult2]);
@@ -397,6 +416,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult1: SearchAfterAndBulkCreateReturnType = {
success: true,
@@ -407,6 +427,7 @@ describe('utils', () => {
createdSignalsCount: 5,
createdSignals: Array(5).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult2: SearchAfterAndBulkCreateReturnType = {
success: true,
@@ -417,6 +438,7 @@ describe('utils', () => {
createdSignalsCount: 8,
createdSignals: Array(8).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const expectedResult: SearchAfterAndBulkCreateReturnType = {
@@ -428,6 +450,7 @@ describe('utils', () => {
createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3)
createdSignals: Array(16).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineConcurrentResults(existingResult, [newResult2, newResult1]); // two array elements are flipped
@@ -444,6 +467,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult1: SearchAfterAndBulkCreateReturnType = {
success: true,
@@ -454,6 +478,7 @@ describe('utils', () => {
createdSignalsCount: 5,
createdSignals: Array(5).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult2: SearchAfterAndBulkCreateReturnType = {
success: true,
@@ -464,6 +489,7 @@ describe('utils', () => {
createdSignalsCount: 8,
createdSignals: Array(8).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const expectedResult: SearchAfterAndBulkCreateReturnType = {
@@ -475,6 +501,7 @@ describe('utils', () => {
createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3)
createdSignals: Array(16).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineConcurrentResults(existingResult, [newResult1, newResult2]);
@@ -491,6 +518,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult: SearchAfterAndBulkCreateReturnType = {
@@ -502,6 +530,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineConcurrentResults(existingResult, [newResult]);
expect(combinedResults.success).toEqual(true);
@@ -517,6 +546,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult: SearchAfterAndBulkCreateReturnType = {
@@ -528,6 +558,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineConcurrentResults(existingResult, [newResult]);
expect(combinedResults.success).toEqual(false);
@@ -543,6 +574,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult: SearchAfterAndBulkCreateReturnType = {
@@ -554,6 +586,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineConcurrentResults(existingResult, [newResult]);
expect(combinedResults.lastLookBackDate?.toISOString()).toEqual('2020-09-16T03:34:32.390Z');
@@ -569,6 +602,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const newResult: SearchAfterAndBulkCreateReturnType = {
@@ -580,6 +614,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: [],
+ warningMessages: [],
};
const combinedResults = combineConcurrentResults(existingResult, [newResult]);
expect(combinedResults).toEqual(
@@ -600,6 +635,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: ['error 1', 'error 2', 'error 3'],
+ warningMessages: [],
};
const newResult: SearchAfterAndBulkCreateReturnType = {
@@ -611,6 +647,7 @@ describe('utils', () => {
createdSignalsCount: 3,
createdSignals: Array(3).fill(sampleSignalHit()),
errors: ['error 4', 'error 1', 'error 3', 'error 5'],
+ warningMessages: [],
};
const combinedResults = combineConcurrentResults(existingResult, [newResult]);
expect(combinedResults).toEqual(
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
index 47a32915dd83f..4d9fda43f032e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
@@ -75,6 +75,7 @@ export const combineResults = (
lastLookBackDate: newResult.lastLookBackDate,
createdSignalsCount: currentResult.createdSignalsCount + newResult.createdSignalsCount,
createdSignals: [...currentResult.createdSignals, ...newResult.createdSignals],
+ warningMessages: [...currentResult.warningMessages, ...newResult.warningMessages],
errors: [...new Set([...currentResult.errors, ...newResult.errors])],
});
@@ -100,6 +101,7 @@ export const combineConcurrentResults = (
lastLookBackDate,
createdSignalsCount: accum.createdSignalsCount + item.createdSignalsCount,
createdSignals: [...accum.createdSignals, ...item.createdSignals],
+ warningMessages: [...accum.warningMessages, ...item.warningMessages],
errors: [...new Set([...accum.errors, ...item.errors])],
};
},
@@ -112,6 +114,7 @@ export const combineConcurrentResults = (
createdSignalsCount: 0,
createdSignals: [],
errors: [],
+ warningMessages: [],
}
);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts
index 197065f205fc5..08fa2f14a0fd5 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts
@@ -19,20 +19,20 @@ import {
} from '../../../../../../alerting/server';
import { BaseHit } from '../../../../../common/detection_engine/types';
import { TermAggregationBucket } from '../../../types';
-import { RefreshTypes } from '../../types';
-import { singleBulkCreate, SingleBulkCreateResponse } from '../single_bulk_create';
+import { GenericBulkCreateResponse } from '../bulk_create_factory';
import {
calculateThresholdSignalUuid,
getThresholdAggregationParts,
getThresholdTermsHash,
} from '../utils';
-import { BuildRuleMessage } from '../rule_messages';
import type {
MultiAggBucket,
SignalSource,
SignalSearchResponse,
ThresholdSignalHistory,
AlertAttributes,
+ BulkCreate,
+ WrapHits,
} from '../types';
import { ThresholdRuleParams } from '../../schemas/rule_schemas';
@@ -42,14 +42,13 @@ interface BulkCreateThresholdSignalsParams {
services: AlertServices;
inputIndexPattern: string[];
logger: Logger;
- id: string;
filter: unknown;
signalsIndex: string;
- refresh: RefreshTypes;
startedAt: Date;
from: Date;
thresholdSignalHistory: ThresholdSignalHistory;
- buildRuleMessage: BuildRuleMessage;
+ bulkCreate: BulkCreate;
+ wrapHits: WrapHits;
}
const getTransformedHits = (
@@ -76,7 +75,7 @@ const getTransformedHits = (
return [];
}
- const getCombinations = (buckets: TermAggregationBucket[], i: number, field: string) => {
+ const getCombinations = (buckets: TermAggregationBucket[], i: number, field: string | null) => {
return buckets.reduce((acc: MultiAggBucket[], bucket: TermAggregationBucket) => {
if (i < threshold.field.length - 1) {
const nextLevelIdx = i + 1;
@@ -100,7 +99,7 @@ const getTransformedHits = (
topThresholdHits: val.topThresholdHits,
docCount: val.docCount,
};
- acc.push(el);
+ acc.push(el as MultiAggBucket);
});
} else {
const el = {
@@ -121,80 +120,76 @@ const getTransformedHits = (
topThresholdHits: bucket.top_threshold_hits,
docCount: bucket.doc_count,
};
- acc.push(el);
+ acc.push(el as MultiAggBucket);
}
return acc;
}, []);
};
- // Recurse through the nested buckets and collect each unique combination of terms. Collect the
- // cardinality and document count from the leaf buckets and return a signal for each set of terms.
- // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response
- return getCombinations(results.aggregations![aggParts.name].buckets, 0, aggParts.field).reduce(
- (acc: Array>, bucket) => {
- const hit = bucket.topThresholdHits?.hits.hits[0];
- if (hit == null) {
- return acc;
- }
-
- const timestampArray = get(timestampOverride ?? '@timestamp', hit.fields);
- if (timestampArray == null) {
- return acc;
- }
-
- const timestamp = timestampArray[0];
- if (typeof timestamp !== 'string') {
- return acc;
- }
-
- const termsHash = getThresholdTermsHash(bucket.terms);
- const signalHit = thresholdSignalHistory[termsHash];
-
- const source = {
- '@timestamp': timestamp,
- ...bucket.terms.reduce((termAcc, term) => {
- if (!term.field.startsWith('signal.')) {
- return {
- ...termAcc,
- [term.field]: term.value,
- };
- }
- return termAcc;
- }, {}),
- threshold_result: {
- terms: bucket.terms,
- cardinality: bucket.cardinality,
- count: bucket.docCount,
- // Store `from` in the signal so that we know the lower bound for the
- // threshold set in the timeline search. The upper bound will always be
- // the `original_time` of the signal (the timestamp of the latest event
- // in the set).
- from:
- signalHit?.lastSignalTimestamp != null
- ? new Date(signalHit!.lastSignalTimestamp)
- : from,
- },
- };
+ return getCombinations(
+ (results.aggregations![aggParts.name] as { buckets: TermAggregationBucket[] }).buckets,
+ 0,
+ aggParts.field
+ ).reduce((acc: Array>, bucket) => {
+ const hit = bucket.topThresholdHits?.hits.hits[0];
+ if (hit == null) {
+ return acc;
+ }
- acc.push({
- _index: inputIndex,
- _id: calculateThresholdSignalUuid(
- ruleId,
- startedAt,
- threshold.field,
- bucket.terms
- .map((term) => term.value)
- .sort()
- .join(',')
- ),
- _source: source,
- });
+ const timestampArray = get(timestampOverride ?? '@timestamp', hit.fields);
+ if (timestampArray == null) {
+ return acc;
+ }
+ const timestamp = timestampArray[0];
+ if (typeof timestamp !== 'string') {
return acc;
- },
- []
- );
+ }
+
+ const termsHash = getThresholdTermsHash(bucket.terms);
+ const signalHit = thresholdSignalHistory[termsHash];
+
+ const source = {
+ '@timestamp': timestamp,
+ ...bucket.terms.reduce((termAcc, term) => {
+ if (!term.field.startsWith('signal.')) {
+ return {
+ ...termAcc,
+ [term.field]: term.value,
+ };
+ }
+ return termAcc;
+ }, {}),
+ threshold_result: {
+ terms: bucket.terms,
+ cardinality: bucket.cardinality,
+ count: bucket.docCount,
+ // Store `from` in the signal so that we know the lower bound for the
+ // threshold set in the timeline search. The upper bound will always be
+ // the `original_time` of the signal (the timestamp of the latest event
+ // in the set).
+ from:
+ signalHit?.lastSignalTimestamp != null ? new Date(signalHit!.lastSignalTimestamp) : from,
+ },
+ };
+
+ acc.push({
+ _index: inputIndex,
+ _id: calculateThresholdSignalUuid(
+ ruleId,
+ startedAt,
+ threshold.field,
+ bucket.terms
+ .map((term) => term.value)
+ .sort()
+ .join(',')
+ ),
+ _source: source,
+ });
+
+ return acc;
+ }, []);
};
export const transformThresholdResultsToEcs = (
@@ -238,7 +233,7 @@ export const transformThresholdResultsToEcs = (
export const bulkCreateThresholdSignals = async (
params: BulkCreateThresholdSignalsParams
-): Promise => {
+): Promise> => {
const ruleParams = params.ruleSO.attributes.params;
const thresholdResults = params.someResult;
const ecsResults = transformThresholdResultsToEcs(
@@ -253,7 +248,6 @@ export const bulkCreateThresholdSignals = async (
ruleParams.timestampOverride,
params.thresholdSignalHistory
);
- const buildRuleMessage = params.buildRuleMessage;
- return singleBulkCreate({ ...params, filteredEvents: ecsResults, buildRuleMessage });
+ return params.bulkCreate(params.wrapHits(ecsResults.hits.hits));
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
index 4205c2d6d8b2c..c35eb04ba1270 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
@@ -26,12 +26,12 @@ import {
RuleAlertAction,
SearchTypes,
} from '../../../../common/detection_engine/types';
-import { RefreshTypes } from '../types';
import { ListClient } from '../../../../../lists/server';
import { Logger, SavedObject } from '../../../../../../../src/core/server';
import { BuildRuleMessage } from './rule_messages';
import { TelemetryEventsSender } from '../../telemetry/sender';
import { RuleParams } from '../schemas/rule_schemas';
+import { GenericBulkCreateResponse } from './bulk_create_factory';
// used for gap detection code
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -255,6 +255,12 @@ export interface QueryFilter {
export type SignalsEnrichment = (signals: SignalSearchResponse) => Promise;
+export type BulkCreate = (docs: Array>) => Promise>;
+
+export type WrapHits = (
+ hits: Array>
+) => Array>;
+
export interface SearchAfterAndBulkCreateParams {
tuples: Array<{
to: moment.Moment;
@@ -272,9 +278,10 @@ export interface SearchAfterAndBulkCreateParams {
signalsIndex: string;
pageSize: number;
filter: unknown;
- refresh: RefreshTypes;
buildRuleMessage: BuildRuleMessage;
enrichment?: SignalsEnrichment;
+ bulkCreate: BulkCreate;
+ wrapHits: WrapHits;
}
export interface SearchAfterAndBulkCreateReturnType {
@@ -284,8 +291,9 @@ export interface SearchAfterAndBulkCreateReturnType {
bulkCreateTimes: string[];
lastLookBackDate: Date | null | undefined;
createdSignalsCount: number;
- createdSignals: SignalHit[];
+ createdSignals: unknown[];
errors: string[];
+ warningMessages: string[];
totalToFromTuples?: Array<{
to: Moment | undefined;
from: Moment | undefined;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
index 60bf0ec337f3d..616cf714d6a8c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
@@ -1083,6 +1083,7 @@ describe('utils', () => {
searchAfterTimes: [],
success: true,
warning: false,
+ warningMessages: [],
};
expect(newSearchResult).toEqual(expected);
});
@@ -1102,6 +1103,7 @@ describe('utils', () => {
searchAfterTimes: [],
success: true,
warning: false,
+ warningMessages: [],
};
expect(newSearchResult).toEqual(expected);
});
@@ -1380,6 +1382,7 @@ describe('utils', () => {
searchAfterTimes: [],
success: true,
warning: false,
+ warningMessages: [],
};
expect(searchAfterReturnType).toEqual(expected);
});
@@ -1394,6 +1397,7 @@ describe('utils', () => {
searchAfterTimes: ['123'],
success: false,
warning: true,
+ warningMessages: ['test warning'],
});
const expected: SearchAfterAndBulkCreateReturnType = {
bulkCreateTimes: ['123'],
@@ -1404,6 +1408,7 @@ describe('utils', () => {
searchAfterTimes: ['123'],
success: false,
warning: true,
+ warningMessages: ['test warning'],
};
expect(searchAfterReturnType).toEqual(expected);
});
@@ -1423,6 +1428,7 @@ describe('utils', () => {
searchAfterTimes: [],
success: true,
warning: false,
+ warningMessages: [],
};
expect(searchAfterReturnType).toEqual(expected);
});
@@ -1440,6 +1446,7 @@ describe('utils', () => {
searchAfterTimes: [],
success: true,
warning: false,
+ warningMessages: [],
};
expect(merged).toEqual(expected);
});
@@ -1494,6 +1501,7 @@ describe('utils', () => {
lastLookBackDate: new Date('2020-08-21T18:51:25.193Z'),
searchAfterTimes: ['123'],
success: true,
+ warningMessages: ['warning1'],
}),
createSearchAfterReturnType({
bulkCreateTimes: ['456'],
@@ -1503,6 +1511,8 @@ describe('utils', () => {
lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'),
searchAfterTimes: ['567'],
success: true,
+ warningMessages: ['warning2'],
+ warning: true,
}),
]);
const expected: SearchAfterAndBulkCreateReturnType = {
@@ -1513,7 +1523,8 @@ describe('utils', () => {
lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'), // takes the next lastLookBackDate
searchAfterTimes: ['123', '567'], // concatenates the searchAfterTimes together
success: true, // Defaults to success true is all of it was successful
- warning: false,
+ warning: true,
+ warningMessages: ['warning1', 'warning2'],
};
expect(merged).toEqual(expected);
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
index 1de76f64fabec..6d67bab6eb2f7 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import { createHash } from 'crypto';
import moment from 'moment';
import uuidv5 from 'uuid/v5';
@@ -17,6 +16,7 @@ import type { ListArray, ExceptionListItemSchema } from '@kbn/securitysolution-i
import { MAX_EXCEPTION_LIST_SIZE } from '@kbn/securitysolution-list-constants';
import { hasLargeValueList } from '@kbn/securitysolution-list-utils';
import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils';
+import { ElasticsearchClient } from '@kbn/securitysolution-es-utils';
import {
TimestampOverrideOrUndefined,
Privilege,
@@ -38,6 +38,7 @@ import {
WrappedSignalHit,
RuleRangeTuple,
BaseSignalHit,
+ SignalSourceHit,
} from './types';
import { BuildRuleMessage } from './rule_messages';
import { ShardError } from '../../types';
@@ -163,9 +164,15 @@ export const hasTimestampFields = async (
export const checkPrivileges = async (
services: AlertServices,
indices: string[]
+): Promise =>
+ checkPrivilegesFromEsClient(services.scopedClusterClient.asCurrentUser, indices);
+
+export const checkPrivilegesFromEsClient = async (
+ esClient: ElasticsearchClient,
+ indices: string[]
): Promise =>
(
- await services.scopedClusterClient.asCurrentUser.transport.request({
+ await esClient.transport.request({
path: '/_security/user/_has_privileges',
method: 'POST',
body: {
@@ -608,7 +615,7 @@ export const getValidDateFromDoc = ({
doc.fields != null && doc.fields[timestamp] != null
? doc.fields[timestamp][0]
: doc._source != null
- ? doc._source[timestamp]
+ ? (doc._source as { [key: string]: unknown })[timestamp]
: undefined;
const lastTimestamp =
typeof timestampValue === 'string' || typeof timestampValue === 'number'
@@ -657,6 +664,7 @@ export const createSearchAfterReturnType = ({
createdSignalsCount,
createdSignals,
errors,
+ warningMessages,
}: {
success?: boolean | undefined;
warning?: boolean;
@@ -664,8 +672,9 @@ export const createSearchAfterReturnType = ({
bulkCreateTimes?: string[] | undefined;
lastLookBackDate?: Date | undefined;
createdSignalsCount?: number | undefined;
- createdSignals?: SignalHit[] | undefined;
+ createdSignals?: unknown[] | undefined;
errors?: string[] | undefined;
+ warningMessages?: string[] | undefined;
} = {}): SearchAfterAndBulkCreateReturnType => {
return {
success: success ?? true,
@@ -676,10 +685,12 @@ export const createSearchAfterReturnType = ({
createdSignalsCount: createdSignalsCount ?? 0,
createdSignals: createdSignals ?? [],
errors: errors ?? [],
+ warningMessages: warningMessages ?? [],
};
};
export const createSearchResultReturnType = (): SignalSearchResponse => {
+ const hits: SignalSourceHit[] = [];
return {
took: 0,
timed_out: false,
@@ -693,7 +704,7 @@ export const createSearchResultReturnType = (): SignalSearchResponse => {
hits: {
total: 0,
max_score: 0,
- hits: [],
+ hits,
},
};
};
@@ -711,7 +722,8 @@ export const mergeReturns = (
createdSignalsCount: existingCreatedSignalsCount,
createdSignals: existingCreatedSignals,
errors: existingErrors,
- } = prev;
+ warningMessages: existingWarningMessages,
+ }: SearchAfterAndBulkCreateReturnType = prev;
const {
success: newSuccess,
@@ -722,7 +734,8 @@ export const mergeReturns = (
createdSignalsCount: newCreatedSignalsCount,
createdSignals: newCreatedSignals,
errors: newErrors,
- } = next;
+ warningMessages: newWarningMessages,
+ }: SearchAfterAndBulkCreateReturnType = next;
return {
success: existingSuccess && newSuccess,
@@ -733,6 +746,7 @@ export const mergeReturns = (
createdSignalsCount: existingCreatedSignalsCount + newCreatedSignalsCount,
createdSignals: [...existingCreatedSignals, ...newCreatedSignals],
errors: [...new Set([...existingErrors, ...newErrors])],
+ warningMessages: [...existingWarningMessages, ...newWarningMessages],
};
});
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts
new file mode 100644
index 0000000000000..3f3e4ef3631bd
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ SearchAfterAndBulkCreateParams,
+ SignalSourceHit,
+ WrapHits,
+ WrappedSignalHit,
+} from './types';
+import { generateId } from './utils';
+import { buildBulkBody } from './build_bulk_body';
+import { filterDuplicateSignals } from './filter_duplicate_signals';
+
+export const wrapHitsFactory = ({
+ ruleSO,
+ signalsIndex,
+}: {
+ ruleSO: SearchAfterAndBulkCreateParams['ruleSO'];
+ signalsIndex: string;
+}): WrapHits => (events) => {
+ const wrappedDocs: WrappedSignalHit[] = events.flatMap((doc) => [
+ {
+ _index: signalsIndex,
+ // TODO: bring back doc._version
+ _id: generateId(doc._index, doc._id, '', ruleSO.attributes.params.ruleId ?? ''),
+ _source: buildBulkBody(ruleSO, doc as SignalSourceHit),
+ },
+ ]);
+
+ return filterDuplicateSignals(ruleSO.id, wrappedDocs);
+};
diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts
index f1c7a275e162c..6ef51bc3c53d4 100644
--- a/x-pack/plugins/security_solution/server/lib/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/types.ts
@@ -17,6 +17,7 @@ import { Notes } from './timeline/saved_object/notes';
import { PinnedEvent } from './timeline/saved_object/pinned_events';
import { Timeline } from './timeline/saved_object/timelines';
import { TotalValue, BaseHit, Explanation } from '../../common/detection_engine/types';
+import { SignalHit } from './detection_engine/signals/types';
export interface AppDomainLibs {
fields: IndexFields;
@@ -100,6 +101,8 @@ export interface SearchResponse extends BaseSearchResponse {
export type SearchHit = SearchResponse['hits']['hits'][0];
+export type SearchSignalHit = SearchResponse['hits']['hits'][0];
+
export interface TermAggregationBucket {
key: string;
doc_count: number;
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 609ee88c319f9..a0f466512cc1d 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -260,6 +260,8 @@ export class Plugin implements IPlugin = {
+ buildDsl: buildEventEnrichmentQuery,
+ parse: parseEventEnrichmentResponse,
+};
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.test.ts
new file mode 100644
index 0000000000000..a246b66d462ce
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.test.ts
@@ -0,0 +1,172 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { buildIndicatorEnrichments, buildIndicatorShouldClauses, getTotalCount } from './helpers';
+
+describe('buildIndicatorShouldClauses', () => {
+ it('returns an empty array given an empty fieldset', () => {
+ expect(buildIndicatorShouldClauses({})).toEqual([]);
+ });
+
+ it('returns an empty array given no relevant values', () => {
+ const eventFields = { 'url.domain': 'elastic.co' };
+ expect(buildIndicatorShouldClauses(eventFields)).toEqual([]);
+ });
+
+ it('returns a clause for each relevant value', () => {
+ const eventFields = { 'source.ip': '127.0.0.1', 'url.full': 'elastic.co' };
+ expect(buildIndicatorShouldClauses(eventFields)).toHaveLength(2);
+ });
+
+ it('excludes non-CTI fields', () => {
+ const eventFields = { 'source.ip': '127.0.0.1', 'url.domain': 'elastic.co' };
+ expect(buildIndicatorShouldClauses(eventFields)).toHaveLength(1);
+ });
+
+ it('defines a named query where the name is the event field and the value is the event field value', () => {
+ const eventFields = { 'file.hash.md5': '1eee2bf3f56d8abed72da2bc523e7431' };
+
+ expect(buildIndicatorShouldClauses(eventFields)).toContainEqual({
+ match: {
+ 'threatintel.indicator.file.hash.md5': {
+ _name: 'file.hash.md5',
+ query: '1eee2bf3f56d8abed72da2bc523e7431',
+ },
+ },
+ });
+ });
+
+ it('returns valid queries for multiple valid fields', () => {
+ const eventFields = { 'source.ip': '127.0.0.1', 'url.full': 'elastic.co' };
+ expect(buildIndicatorShouldClauses(eventFields)).toEqual(
+ expect.arrayContaining([
+ { match: { 'threatintel.indicator.ip': { _name: 'source.ip', query: '127.0.0.1' } } },
+ { match: { 'threatintel.indicator.url.full': { _name: 'url.full', query: 'elastic.co' } } },
+ ])
+ );
+ });
+});
+
+describe('getTotalCount', () => {
+ it('returns 0 when total is null (not tracking)', () => {
+ expect(getTotalCount(null)).toEqual(0);
+ });
+
+ it('returns total when total is a number', () => {
+ expect(getTotalCount(5)).toEqual(5);
+ });
+
+ it('returns total.value when total is an object', () => {
+ expect(getTotalCount({ value: 20, relation: 'eq' })).toEqual(20);
+ });
+});
+
+describe('buildIndicatorEnrichments', () => {
+ it('returns nothing if hits have no matched queries', () => {
+ const hits = [{ _id: '_id', _index: '_index', matched_queries: [] }];
+ expect(buildIndicatorEnrichments(hits)).toEqual([]);
+ });
+
+ it("returns nothing if hits' matched queries are not valid", () => {
+ const hits = [{ _id: '_id', _index: '_index', matched_queries: ['invalid.field'] }];
+ expect(buildIndicatorEnrichments(hits)).toEqual([]);
+ });
+
+ it('builds a single enrichment if the hit has a matched query', () => {
+ const hits = [
+ {
+ _id: '_id',
+ _index: '_index',
+ matched_queries: ['file.hash.md5'],
+ fields: {
+ 'threatintel.indicator.file.hash.md5': ['indicator_value'],
+ },
+ },
+ ];
+
+ expect(buildIndicatorEnrichments(hits)).toEqual([
+ expect.objectContaining({
+ 'matched.atomic': ['indicator_value'],
+ 'matched.field': ['file.hash.md5'],
+ 'matched.id': ['_id'],
+ 'matched.index': ['_index'],
+ 'threatintel.indicator.file.hash.md5': ['indicator_value'],
+ }),
+ ]);
+ });
+
+ it('builds multiple enrichments if the hit has matched queries', () => {
+ const hits = [
+ {
+ _id: '_id',
+ _index: '_index',
+ matched_queries: ['file.hash.md5', 'source.ip'],
+ fields: {
+ 'threatintel.indicator.file.hash.md5': ['indicator_value'],
+ 'threatintel.indicator.ip': ['127.0.0.1'],
+ },
+ },
+ ];
+
+ expect(buildIndicatorEnrichments(hits)).toEqual([
+ expect.objectContaining({
+ 'matched.atomic': ['indicator_value'],
+ 'matched.field': ['file.hash.md5'],
+ 'matched.id': ['_id'],
+ 'matched.index': ['_index'],
+ 'threatintel.indicator.file.hash.md5': ['indicator_value'],
+ 'threatintel.indicator.ip': ['127.0.0.1'],
+ }),
+ expect.objectContaining({
+ 'matched.atomic': ['127.0.0.1'],
+ 'matched.field': ['source.ip'],
+ 'matched.id': ['_id'],
+ 'matched.index': ['_index'],
+ 'threatintel.indicator.file.hash.md5': ['indicator_value'],
+ 'threatintel.indicator.ip': ['127.0.0.1'],
+ }),
+ ]);
+ });
+
+ it('builds an enrichment for each hit', () => {
+ const hits = [
+ {
+ _id: '_id',
+ _index: '_index',
+ matched_queries: ['file.hash.md5'],
+ fields: {
+ 'threatintel.indicator.file.hash.md5': ['indicator_value'],
+ },
+ },
+ {
+ _id: '_id2',
+ _index: '_index2',
+ matched_queries: ['source.ip'],
+ fields: {
+ 'threatintel.indicator.ip': ['127.0.0.1'],
+ },
+ },
+ ];
+
+ expect(buildIndicatorEnrichments(hits)).toEqual([
+ expect.objectContaining({
+ 'matched.atomic': ['indicator_value'],
+ 'matched.field': ['file.hash.md5'],
+ 'matched.id': ['_id'],
+ 'matched.index': ['_index'],
+ 'threatintel.indicator.file.hash.md5': ['indicator_value'],
+ }),
+ expect.objectContaining({
+ 'matched.atomic': ['127.0.0.1'],
+ 'matched.field': ['source.ip'],
+ 'matched.id': ['_id2'],
+ 'matched.index': ['_index2'],
+ 'threatintel.indicator.ip': ['127.0.0.1'],
+ }),
+ ]);
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts
new file mode 100644
index 0000000000000..e4ed05baeed77
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts
@@ -0,0 +1,83 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { get, isEmpty } from 'lodash';
+import { estypes } from '@elastic/elasticsearch';
+
+import { EVENT_ENRICHMENT_INDICATOR_FIELD_MAP } from '../../../../../../common/cti/constants';
+import { CtiEnrichment } from '../../../../../../common/search_strategy/security_solution/cti';
+
+type EventField = keyof typeof EVENT_ENRICHMENT_INDICATOR_FIELD_MAP;
+const validEventFields = Object.keys(EVENT_ENRICHMENT_INDICATOR_FIELD_MAP) as EventField[];
+
+const isValidEventField = (field: string): field is EventField =>
+ validEventFields.includes(field as EventField);
+
+export const buildIndicatorShouldClauses = (
+ eventFields: Record
+): estypes.QueryDslQueryContainer[] => {
+ return validEventFields.reduce((shoulds, eventField) => {
+ const eventFieldValue = eventFields[eventField];
+
+ if (!isEmpty(eventFieldValue)) {
+ shoulds.push({
+ match: {
+ [EVENT_ENRICHMENT_INDICATOR_FIELD_MAP[eventField]]: {
+ query: eventFieldValue,
+ _name: eventField,
+ },
+ },
+ });
+ }
+
+ return shoulds;
+ }, []);
+};
+
+export const buildIndicatorEnrichments = (hits: estypes.SearchHit[]): CtiEnrichment[] => {
+ return hits.flatMap(({ matched_queries: matchedQueries, ...hit }) => {
+ return (
+ matchedQueries?.reduce((enrichments, matchedQuery) => {
+ if (isValidEventField(matchedQuery)) {
+ enrichments.push({
+ ...hit.fields,
+ ...buildIndicatorMatchedFields(hit, matchedQuery),
+ });
+ }
+
+ return enrichments;
+ }, []) ?? []
+ );
+ });
+};
+
+const buildIndicatorMatchedFields = (
+ hit: estypes.SearchHit,
+ eventField: EventField
+): Record => {
+ const indicatorField = EVENT_ENRICHMENT_INDICATOR_FIELD_MAP[eventField];
+ const atomic = get(hit.fields, indicatorField) as string[];
+
+ return {
+ 'matched.atomic': atomic,
+ 'matched.field': [eventField],
+ 'matched.id': [hit._id],
+ 'matched.index': [hit._index],
+ };
+};
+
+export const getTotalCount = (total: number | estypes.SearchTotalHits | null): number => {
+ if (total == null) {
+ return 0;
+ }
+
+ if (typeof total === 'number') {
+ return total;
+ }
+
+ return total.value;
+};
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/index.ts
new file mode 100644
index 0000000000000..6884b7b6320cf
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { eventEnrichment } from './factory';
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.test.ts
new file mode 100644
index 0000000000000..bc96a387105c6
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.test.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { buildEventEnrichmentRequestOptionsMock } from '../../../../../../common/search_strategy/security_solution/cti/index.mock';
+import { buildEventEnrichmentQuery } from './query';
+
+describe('buildEventEnrichmentQuery', () => {
+ it('converts each event field/value into a named filter', () => {
+ const options = buildEventEnrichmentRequestOptionsMock();
+ const query = buildEventEnrichmentQuery(options);
+ expect(query.body?.query?.bool?.should).toEqual(
+ expect.arrayContaining([
+ {
+ match: {
+ 'threatintel.indicator.file.hash.md5': {
+ _name: 'file.hash.md5',
+ query: '1eee2bf3f56d8abed72da2bc523e7431',
+ },
+ },
+ },
+ { match: { 'threatintel.indicator.ip': { _name: 'source.ip', query: '127.0.0.1' } } },
+ { match: { 'threatintel.indicator.url.full': { _name: 'url.full', query: 'elastic.co' } } },
+ ])
+ );
+ });
+
+ it('filters on indicator events', () => {
+ const options = buildEventEnrichmentRequestOptionsMock();
+ const query = buildEventEnrichmentQuery(options);
+ expect(query.body?.query?.bool?.filter).toEqual(
+ expect.arrayContaining([{ term: { 'event.type': 'indicator' } }])
+ );
+ });
+
+ it('includes the specified timerange', () => {
+ const options = buildEventEnrichmentRequestOptionsMock();
+ const query = buildEventEnrichmentQuery(options);
+ expect(query.body?.query?.bool?.filter).toEqual(
+ expect.arrayContaining([
+ {
+ range: {
+ '@timestamp': {
+ format: 'strict_date_optional_time',
+ gte: '2020-09-13T09:00:43.249Z',
+ lte: '2020-09-14T09:00:43.249Z',
+ },
+ },
+ },
+ ])
+ );
+ });
+
+ it('includes specified docvalue_fields', () => {
+ const docValueFields = [
+ { field: '@timestamp', format: 'date_time' },
+ { field: 'event.created', format: 'date_time' },
+ { field: 'event.end', format: 'date_time' },
+ ];
+ const options = buildEventEnrichmentRequestOptionsMock({ docValueFields });
+ const query = buildEventEnrichmentQuery(options);
+ expect(query.body?.docvalue_fields).toEqual(expect.arrayContaining(docValueFields));
+ });
+
+ it('requests all fields', () => {
+ const options = buildEventEnrichmentRequestOptionsMock();
+ const query = buildEventEnrichmentQuery(options);
+ expect(query.body?.fields).toEqual(['*']);
+ });
+
+ it('excludes _source', () => {
+ const options = buildEventEnrichmentRequestOptionsMock();
+ const query = buildEventEnrichmentQuery(options);
+ expect(query.body?._source).toEqual(false);
+ });
+
+ it('includes specified filters', () => {
+ const filterQuery = {
+ query: 'query_field: query_value',
+ language: 'kuery',
+ };
+
+ const options = buildEventEnrichmentRequestOptionsMock({ filterQuery });
+ const query = buildEventEnrichmentQuery(options);
+ expect(query.body?.query?.bool?.filter).toEqual(expect.arrayContaining([filterQuery]));
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.ts
new file mode 100644
index 0000000000000..4760e6a227cd3
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { isEmpty } from 'lodash';
+import { CtiQueries } from '../../../../../../common/search_strategy/security_solution/cti';
+import { createQueryFilterClauses } from '../../../../../utils/build_query';
+import { SecuritySolutionFactory } from '../../types';
+import { buildIndicatorShouldClauses } from './helpers';
+
+export const buildEventEnrichmentQuery: SecuritySolutionFactory['buildDsl'] = ({
+ defaultIndex,
+ docValueFields,
+ eventFields,
+ filterQuery,
+ timerange: { from, to },
+}) => {
+ const filter = [
+ ...createQueryFilterClauses(filterQuery),
+ { term: { 'event.type': 'indicator' } },
+ {
+ range: {
+ '@timestamp': {
+ gte: from,
+ lte: to,
+ format: 'strict_date_optional_time',
+ },
+ },
+ },
+ ];
+
+ return {
+ allowNoIndices: true,
+ ignoreUnavailable: true,
+ index: defaultIndex,
+ body: {
+ _source: false,
+ ...(!isEmpty(docValueFields) && { docvalue_fields: docValueFields }),
+ fields: ['*'],
+ query: {
+ bool: {
+ should: buildIndicatorShouldClauses(eventFields),
+ filter,
+ minimum_should_match: 1,
+ },
+ },
+ },
+ };
+};
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/response.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/response.test.ts
new file mode 100644
index 0000000000000..7ced866e0bb5b
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/response.test.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ buildEventEnrichmentRequestOptionsMock,
+ buildEventEnrichmentRawResponseMock,
+} from '../../../../../../common/search_strategy/security_solution/cti/index.mock';
+import { parseEventEnrichmentResponse } from './response';
+
+describe('parseEventEnrichmentResponse', () => {
+ it('includes an accurate inspect response', async () => {
+ const options = buildEventEnrichmentRequestOptionsMock();
+ const response = buildEventEnrichmentRawResponseMock();
+ const parsedResponse = await parseEventEnrichmentResponse(options, response);
+
+ const expectedInspect = expect.objectContaining({
+ allowNoIndices: true,
+ body: {
+ _source: false,
+ fields: ['*'],
+ query: {
+ bool: {
+ filter: [
+ { bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } },
+ { term: { 'event.type': 'indicator' } },
+ {
+ range: {
+ '@timestamp': {
+ format: 'strict_date_optional_time',
+ gte: '2020-09-13T09:00:43.249Z',
+ lte: '2020-09-14T09:00:43.249Z',
+ },
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ should: [
+ {
+ match: {
+ 'threatintel.indicator.file.hash.md5': {
+ _name: 'file.hash.md5',
+ query: '1eee2bf3f56d8abed72da2bc523e7431',
+ },
+ },
+ },
+ { match: { 'threatintel.indicator.ip': { _name: 'source.ip', query: '127.0.0.1' } } },
+ {
+ match: {
+ 'threatintel.indicator.url.full': { _name: 'url.full', query: 'elastic.co' },
+ },
+ },
+ ],
+ },
+ },
+ },
+ ignoreUnavailable: true,
+ index: ['filebeat-*'],
+ });
+ const parsedInspect = JSON.parse(parsedResponse.inspect!.dsl[0]);
+ expect(parsedInspect).toEqual(expectedInspect);
+ });
+
+ it('includes an accurate total count', async () => {
+ const options = buildEventEnrichmentRequestOptionsMock();
+ const response = buildEventEnrichmentRawResponseMock();
+ const parsedResponse = await parseEventEnrichmentResponse(options, response);
+
+ expect(parsedResponse.totalCount).toEqual(1);
+ });
+
+ it('adds matched.* enrichment fields based on the named query', async () => {
+ const options = buildEventEnrichmentRequestOptionsMock();
+ const response = buildEventEnrichmentRawResponseMock();
+ const parsedResponse = await parseEventEnrichmentResponse(options, response);
+
+ expect(parsedResponse.enrichments).toEqual([
+ expect.objectContaining({
+ 'matched.atomic': ['5529de7b60601aeb36f57824ed0e1ae8'],
+ 'matched.field': ['file.hash.md5'],
+ 'matched.id': ['31408415b6d5601a92d29b86c2519658f210c194057588ae396d55cc20b3f03d'],
+ 'matched.index': ['filebeat-8.0.0-2021.05.28-000001'],
+ }),
+ ]);
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/response.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/response.ts
new file mode 100644
index 0000000000000..29a842d84558c
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/response.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { CtiQueries } from '../../../../../../common/search_strategy/security_solution/cti';
+import { inspectStringifyObject } from '../../../../../utils/build_query';
+import { SecuritySolutionFactory } from '../../types';
+import { buildIndicatorEnrichments, getTotalCount } from './helpers';
+import { buildEventEnrichmentQuery } from './query';
+
+export const parseEventEnrichmentResponse: SecuritySolutionFactory['parse'] = async (
+ options,
+ response,
+ deps
+) => {
+ const inspect = {
+ dsl: [inspectStringifyObject(buildEventEnrichmentQuery(options))],
+ };
+ const totalCount = getTotalCount(response.rawResponse.hits.total);
+ const enrichments = buildIndicatorEnrichments(response.rawResponse.hits.hits);
+
+ return {
+ ...response,
+ enrichments,
+ inspect,
+ totalCount,
+ };
+};
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts
new file mode 100644
index 0000000000000..5857a0417239c
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { FactoryQueryTypes } from '../../../../../common/search_strategy/security_solution';
+import { CtiQueries } from '../../../../../common/search_strategy/security_solution/cti';
+import type { SecuritySolutionFactory } from '../types';
+import { eventEnrichment } from './event_enrichment';
+
+export const ctiFactoryTypes: Record> = {
+ [CtiQueries.eventEnrichment]: eventEnrichment,
+};
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts
index 346dd20c89441..5b54c63408d10 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts
@@ -5,12 +5,13 @@
* 2.0.
*/
-import { FactoryQueryTypes } from '../../../../common/search_strategy/security_solution';
+import type { FactoryQueryTypes } from '../../../../common/search_strategy/security_solution';
+import type { SecuritySolutionFactory } from './types';
import { hostsFactory } from './hosts';
import { matrixHistogramFactory } from './matrix_histogram';
import { networkFactory } from './network';
-import { SecuritySolutionFactory } from './types';
+import { ctiFactoryTypes } from './cti';
export const securitySolutionFactory: Record<
FactoryQueryTypes,
@@ -19,4 +20,5 @@ export const securitySolutionFactory: Record<
...hostsFactory,
...matrixHistogramFactory,
...networkFactory,
+ ...ctiFactoryTypes,
};
diff --git a/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts
index ebcda69441135..8d5a2efc7fae1 100644
--- a/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts
+++ b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts
@@ -177,6 +177,8 @@ export const updateDetectionRuleUsage = (
return updatedUsage;
};
+const MAX_RESULTS_WINDOW = 10_000; // elasticsearch index.max_result_window default value
+
export const getDetectionRuleMetrics = async (
kibanaIndex: string,
signalsIndex: string,
@@ -189,14 +191,14 @@ export const getDetectionRuleMetrics = async (
filterPath: [],
ignoreUnavailable: true,
index: kibanaIndex,
- size: 10_000, // elasticsearch index.max_result_window default value
+ size: MAX_RESULTS_WINDOW,
};
try {
const { body: ruleResults } = await esClient.search(ruleSearchOptions);
const { body: detectionAlertsResp } = (await esClient.search({
index: `${signalsIndex}*`,
- size: 0,
+ size: MAX_RESULTS_WINDOW,
body: {
aggs: {
detectionAlerts: {
@@ -224,7 +226,7 @@ export const getDetectionRuleMetrics = async (
type: 'cases-comments',
fields: [],
page: 1,
- perPage: 10_000,
+ perPage: MAX_RESULTS_WINDOW,
filter: 'cases-comments.attributes.type: alert',
});
diff --git a/x-pack/plugins/snapshot_restore/public/application/app.tsx b/x-pack/plugins/snapshot_restore/public/application/app.tsx
index 0c064d448105f..a7993300079ab 100644
--- a/x-pack/plugins/snapshot_restore/public/application/app.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/app.tsx
@@ -10,14 +10,16 @@ import { Redirect, Route, Switch } from 'react-router-dom';
import { EuiPageContent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { APP_WRAPPER_CLASS } from '../../../../../src/core/public';
+
import { APP_REQUIRED_CLUSTER_PRIVILEGES } from '../../common';
import {
useAuthorizationContext,
- SectionError,
+ PageError,
WithPrivileges,
NotAuthorizedSection,
} from '../shared_imports';
-import { SectionLoading } from './components';
+import { PageLoading } from './components';
import { DEFAULT_SECTION, Section } from './constants';
import {
RepositoryAdd,
@@ -42,7 +44,7 @@ export const App: React.FunctionComponent = () => {
const sectionsRegex = sections.join('|');
return apiError ? (
- {
`cluster.${name}`)}>
{({ isLoading, hasPrivileges, privilegesMissing }) =>
isLoading ? (
-
+
-
+
) : hasPrivileges ? (
-
+
@@ -84,7 +86,7 @@ export const App: React.FunctionComponent = () => {
) : (
-
+
= ({ children, ...rest }) => {
+ return (
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export const SectionLoading: React.FunctionComponent = ({ children }) => {
+ return (
+ }
+ body={{children} }
+ data-test-subj="sectionLoading"
+ />
+ );
+};
+
+/*
+ * Loading component used for full page loads.
+ * For tabbed sections, or within the context of a wizard,
+ * the component may be more appropriate
+ */
+export const PageLoading: React.FunctionComponent = ({ children }) => {
+ return (
+
+ }
+ body={{children} }
+ data-test-subj="sectionLoading"
+ />
+
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx
index 6443d774c9ac7..06c65a1713692 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx
@@ -30,7 +30,7 @@ import { useCore, useServices } from '../../../app_context';
import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants';
import { useLoadRepositories } from '../../../services/http';
import { linkToAddRepository } from '../../../services/navigation';
-import { SectionLoading } from '../../';
+import { InlineLoading } from '../../';
import { StepProps } from './';
import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public';
@@ -174,12 +174,12 @@ export const PolicyStepLogistics: React.FunctionComponent = ({
const renderRepositorySelect = () => {
if (isLoadingRepositories) {
return (
-
+
-
+
);
}
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/section_loading.tsx b/x-pack/plugins/snapshot_restore/public/application/components/section_loading.tsx
deleted file mode 100644
index c1548ad960bb0..0000000000000
--- a/x-pack/plugins/snapshot_restore/public/application/components/section_loading.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-import {
- EuiEmptyPrompt,
- EuiLoadingSpinner,
- EuiText,
- EuiFlexGroup,
- EuiFlexItem,
- EuiTextColor,
-} from '@elastic/eui';
-
-interface Props {
- inline?: boolean;
- children: React.ReactNode;
- [key: string]: any;
-}
-
-export const SectionLoading: React.FunctionComponent = ({ inline, children, ...rest }) => {
- if (inline) {
- return (
-
-
-
-
-
-
- {children}
-
-
-
- );
- }
-
- return (
- }
- body={{children} }
- data-test-subj="sectionLoading"
- />
- );
-};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx
index e4a23bac636d8..211d30181c25c 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx
@@ -9,18 +9,7 @@ import React, { useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiPageBody,
- EuiPageContent,
- EuiSpacer,
- EuiTab,
- EuiTabs,
- EuiTitle,
- EuiText,
-} from '@elastic/eui';
+import { EuiButtonEmpty, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { BASE_PATH, Section } from '../../constants';
import { useConfig, useCore } from '../../app_context';
@@ -100,79 +89,65 @@ export const SnapshotRestoreHome: React.FunctionComponent
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
-
-
-
-
-
-
- {tabs.map((tab) => (
- onSectionChange(tab.id)}
- isSelected={tab.id === section}
- key={tab.id}
- data-test-subj={tab.id.toLowerCase() + '_tab'}
- >
- {tab.name}
-
- ))}
-
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ description={
+
+ }
+ tabs={tabs.map((tab) => ({
+ onClick: () => onSectionChange(tab.id),
+ isSelected: tab.id === section,
+ key: tab.id,
+ 'data-test-subj': tab.id.toLowerCase() + '_tab',
+ label: tab.name,
+ }))}
+ />
-
+
-
-
- {/* We have two separate SnapshotList routes because repository names could have slashes in
- * them. This would break a route with a path like snapshots/:repositoryName?/:snapshotId*
- */}
-
-
-
-
-
-
-
+
+
+ {/* We have two separate SnapshotList routes because repository names could have slashes in
+ * them. This would break a route with a path like snapshots/:repositoryName?/:snapshotId*
+ */}
+
+
+
+
+
+ >
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx
index 2bad30b95081d..0a283d406e5aa 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx
@@ -40,6 +40,7 @@ import { linkToEditPolicy, linkToSnapshot } from '../../../../services/navigatio
import {
SectionLoading,
+ InlineLoading,
PolicyExecuteProvider,
PolicyDeleteProvider,
} from '../../../../components';
@@ -318,7 +319,7 @@ export const PolicyDetails: React.FunctionComponent = ({
{policyDetails && policyDetails.policy && policyDetails.policy.inProgress ? (
<>
-
+
= ({
values={{ snapshotName: policyDetails.policy.inProgress.snapshotName }}
/>
-
+
>
) : null}
{renderTabs()}
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx
index 7b1c10ec59e8a..3927b73abf093 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx
@@ -9,13 +9,13 @@ import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { RouteComponentProps } from 'react-router-dom';
-import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { EuiPageContentBody, EuiSpacer, EuiPageHeader } from '@elastic/eui';
import { SlmPolicyPayload } from '../../../../common/types';
import { TIME_UNITS } from '../../../../common';
-import { SectionError, Error } from '../../../shared_imports';
+import { SectionError, PageError } from '../../../shared_imports';
-import { PolicyForm, SectionLoading } from '../../components';
+import { PolicyForm, PageLoading } from '../../components';
import { BASE_PATH, DEFAULT_POLICY_SCHEDULE } from '../../constants';
import { breadcrumbService, docTitleService } from '../../services/navigation';
import { addPolicy, useLoadIndices } from '../../services/http';
@@ -87,49 +87,57 @@ export const PolicyAdd: React.FunctionComponent = ({
setSaveError(null);
};
+ if (isLoadingIndices) {
+ return (
+
+
+
+ );
+ }
+
+ if (errorLoadingIndices) {
+ return (
+
+ }
+ error={errorLoadingIndices}
+ />
+ );
+ }
+
return (
-
-
-
-
+